diff --git a/Core/AppConfigurationURLProvider.swift b/Core/AppConfigurationURLProvider.swift index 0db485bffc..db84a448c0 100644 --- a/Core/AppConfigurationURLProvider.swift +++ b/Core/AppConfigurationURLProvider.swift @@ -20,19 +20,32 @@ import Foundation import Configuration import Core +import BrowserServicesKit struct AppConfigurationURLProvider: ConfigurationURLProviding { + private let trackerDataUrlProvider: TrackerDataURLProviding + + /// Default initializer using shared dependencies. + init (privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + featureFlagger: FeatureFlagger = AppDependencyProvider.shared.featureFlagger) { + self.trackerDataUrlProvider = TrackerDataURLOverrider(privacyConfigurationManager: privacyConfigurationManager, featureFlagger: featureFlagger) + } + + /// Initializer for injecting a custom TrackerDataURLProvider. + internal init (trackerDataUrlProvider: TrackerDataURLProviding) { + self.trackerDataUrlProvider = trackerDataUrlProvider + } + func url(for configuration: Configuration) -> URL { switch configuration { case .bloomFilterSpec: return URL.bloomFilterSpec case .bloomFilterBinary: return URL.bloomFilter case .bloomFilterExcludedDomains: return URL.bloomFilterExcludedDomains case .privacyConfiguration: return URL.privacyConfig - case .trackerDataSet: return URL.trackerDataSet + case .trackerDataSet: return trackerDataUrlProvider.trackerDataURL ?? URL.trackerDataSet case .surrogates: return URL.surrogates case .remoteMessagingConfig: return RemoteMessagingClient.Constants.endpoint } } - } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 383d1dffc3..e1db32444b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -275,6 +275,9 @@ 4BE67B072B96B9B0007335F7 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 4BE67B062B96B9B0007335F7 /* Common */; }; 4BF3E4AF2C06A85200ED5D57 /* VPNRedditSessionWorkaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF3E4AE2C06A85200ED5D57 /* VPNRedditSessionWorkaround.swift */; }; 560E990F2BEE2CB800507CE0 /* SyncErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */; }; + 563A3CFE2D37B8FA001966FD /* ConfigurationManagerIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563A3CFD2D37B8FA001966FD /* ConfigurationManagerIntegrationTests.swift */; }; + 563A3D012D37BF83001966FD /* ConfigurationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563A3D002D37BF83001966FD /* ConfigurationManagerTests.swift */; }; + 563A3D032D37C363001966FD /* AppConfigurationURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563A3D022D37C363001966FD /* AppConfigurationURLProviderTests.swift */; }; 564DE4532C3ED1B700D23241 /* NewTabDaxDialogFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4522C3ED1B700D23241 /* NewTabDaxDialogFactory.swift */; }; 564DE4552C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4542C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift */; }; 564DE4572C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564DE4562C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift */; }; @@ -1678,6 +1681,9 @@ 4BE27566272F878F006B20B0 /* URLRequestExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLRequestExtension.swift; path = ../DuckDuckGo/URLRequestExtension.swift; sourceTree = ""; }; 4BF3E4AE2C06A85200ED5D57 /* VPNRedditSessionWorkaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNRedditSessionWorkaround.swift; sourceTree = ""; }; 560E990E2BEE2CB800507CE0 /* SyncErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorMessage.swift; sourceTree = ""; }; + 563A3CFD2D37B8FA001966FD /* ConfigurationManagerIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManagerIntegrationTests.swift; sourceTree = ""; }; + 563A3D002D37BF83001966FD /* ConfigurationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManagerTests.swift; sourceTree = ""; }; + 563A3D022D37C363001966FD /* AppConfigurationURLProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProviderTests.swift; sourceTree = ""; }; 564DE4522C3ED1B700D23241 /* NewTabDaxDialogFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabDaxDialogFactory.swift; sourceTree = ""; }; 564DE4542C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingNewTabDialogFactoryTests.swift; sourceTree = ""; }; 564DE4562C4150E600D23241 /* NewTabPageControllerDaxDialogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageControllerDaxDialogTests.swift; sourceTree = ""; }; @@ -4066,6 +4072,23 @@ name = LiveActivity; sourceTree = ""; }; + 563A3CFC2D37B8E3001966FD /* Configuration */ = { + isa = PBXGroup; + children = ( + 563A3CFD2D37B8FA001966FD /* ConfigurationManagerIntegrationTests.swift */, + ); + path = Configuration; + sourceTree = ""; + }; + 563A3CFF2D37BF60001966FD /* Configuration */ = { + isa = PBXGroup; + children = ( + 563A3D002D37BF83001966FD /* ConfigurationManagerTests.swift */, + 563A3D022D37C363001966FD /* AppConfigurationURLProviderTests.swift */, + ); + path = Configuration; + sourceTree = ""; + }; 566B736E2BECD3DC00FF1959 /* Utilities */ = { isa = PBXGroup; children = ( @@ -4921,6 +4944,7 @@ 85D33FCC25C97B6E002B91A6 /* IntegrationTests */ = { isa = PBXGroup; children = ( + 563A3CFC2D37B8E3001966FD /* Configuration */, 1E1D8B5F29950FB300C96994 /* Autoconsent */, 85F21DBD21121147002631A6 /* AtbServerTests.swift */, 85519124247468580010FDD0 /* TrackerRadarIntegrationTests.swift */, @@ -6210,6 +6234,7 @@ F12D98401F266B30003C2EE3 /* DuckDuckGo */ = { isa = PBXGroup; children = ( + 563A3CFF2D37BF60001966FD /* Configuration */, 6FF9157F2B88E04F0042AC87 /* AdAttribution */, F17669A21E411D63003D3222 /* Application */, 981FED7222045FFA008488D7 /* AutoClear */, @@ -8500,6 +8525,7 @@ files = ( 8528AE84212FF9A100D0BD74 /* AppRatingPromptStorageTests.swift in Sources */, 569437312BE3F64400C0881B /* SyncErrorHandlerSyncPausedAlertsTests.swift in Sources */, + 563A3D012D37BF83001966FD /* ConfigurationManagerTests.swift in Sources */, 9F16230B2CA0F0190093C4FC /* DebouncerTests.swift in Sources */, 1CB7B82323CEA28300AA24EA /* DateExtensionTests.swift in Sources */, 31C138A427A3352600FFD4B2 /* DownloadTests.swift in Sources */, @@ -8546,6 +8572,7 @@ C158AC7B297AB5DC0008723A /* MockSecureVault.swift in Sources */, 569437342BE4E41500C0881B /* SyncErrorHandlerSyncErrorsAlertsTests.swift in Sources */, 85C11E4120904BBE00BFFEB4 /* VariantManagerTests.swift in Sources */, + 563A3D032D37C363001966FD /* AppConfigurationURLProviderTests.swift in Sources */, F1134ECE1F40EA9C00B73467 /* AtbParserTests.swift in Sources */, F189AEE41F18FDAF001EBAE1 /* LinkTests.swift in Sources */, 6F7FB8E12C660B3E00867DA7 /* NewTabPageFavoritesModelTests.swift in Sources */, @@ -8755,6 +8782,7 @@ 1E1D8B6629953B9800C96994 /* WebViewTestHelper.swift in Sources */, EE3B226C29DE0FD30082298A /* MockInternalUserStoring.swift in Sources */, CB5516D0286500290079B175 /* TrackerRadarIntegrationTests.swift in Sources */, + 563A3CFE2D37B8FA001966FD /* ConfigurationManagerIntegrationTests.swift in Sources */, 1E1D8B6129950FD200C96994 /* AutoconsentBackgroundTests.swift in Sources */, CB5516D2286500290079B175 /* AtbServerTests.swift in Sources */, ); @@ -11933,7 +11961,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 224.7.2; + version = 225.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index dc4f57dc88..a3ea5b38df 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "b3a8ea5ef9821203fe88a12e3f15ad48b7278a6a", - "version" : "224.7.2" + "revision" : "20e6eaf0b1e423d9a270e2d460cae284c08f73d8", + "version" : "225.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "0502ed7de4130bd8705daebaca9aeb20d3e62d15", - "version" : "7.5.0" + "revision" : "7958ddab724c26326333cae13fe81478290607fa", + "version" : "7.6.0" } }, { @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/sync_crypto", "state" : { - "revision" : "0c8bf3c0e75591bc366407b9d7a73a9fcfc7736f", - "version" : "0.3.0" + "revision" : "cc726cebb67367466bc31ced4784e16d44ac68d1", + "version" : "0.4.0" } }, { diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index a34f161f01..c1adebea80 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -27,6 +27,9 @@ import os.log final class ConfigurationManager: DefaultConfigurationManager { + private let trackerDataManager: TrackerDataManager + private let privacyConfigurationManager: PrivacyConfigurationManaging + private enum Constants { static let lastConfigurationInstallDateKey = "config.last.installed" } @@ -71,9 +74,13 @@ final class ConfigurationManager: DefaultConfigurationManager { } } - override init(fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents), - store: ConfigurationStoring = AppDependencyProvider.shared.configurationStore, - defaults: KeyValueStoring = UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults()) { + init(fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents), + store: ConfigurationStoring = AppDependencyProvider.shared.configurationStore, + defaults: KeyValueStoring = UserDefaults(suiteName: "\(Global.groupIdPrefix).app-configuration") ?? UserDefaults(), + trackerDataManager: TrackerDataManager = ContentBlocking.shared.trackerDataManager, + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager) { + self.trackerDataManager = trackerDataManager + self.privacyConfigurationManager = privacyConfigurationManager super.init(fetcher: fetcher, store: store, defaults: defaults) subscribeToLifecycleNotifications() } @@ -117,10 +124,27 @@ final class ConfigurationManager: DefaultConfigurationManager { private func fetchTrackerBlockingDependencies(isDebug: Bool = false) async -> Bool { var didFetchAnyTrackerBlockingDependencies = false - var tasks = [Configuration: Task<(), Swift.Error>]() - tasks[.trackerDataSet] = Task { try await fetcher.fetch(.trackerDataSet, isDebug: isDebug) } - tasks[.surrogates] = Task { try await fetcher.fetch(.surrogates, isDebug: isDebug) } - tasks[.privacyConfiguration] = Task { try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) } + // Start surrogates fetch task + let surrogatesTask = Task { try await fetcher.fetch(.surrogates, isDebug: isDebug) } + + // Perform privacyConfiguration fetch and update + do { + try await fetcher.fetch(.privacyConfiguration, isDebug: isDebug) + didFetchAnyTrackerBlockingDependencies = true + privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration), + data: store.loadData(for: .privacyConfiguration)) + } catch { + Logger.general.error("Did not apply update to \(Configuration.privacyConfiguration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)") + } + + // Start trackerDataSet fetch task after privacyConfiguration completes + let trackerDataSetTask = Task { try await fetcher.fetch(.trackerDataSet, isDebug: isDebug) } + + // Wait for surrogates and trackerDataSet tasks + let tasks: [(Configuration, Task<(), Swift.Error>)] = [ + (.surrogates, surrogatesTask), + (.trackerDataSet, trackerDataSetTask) + ] for (configuration, task) in tasks { do { @@ -135,10 +159,10 @@ final class ConfigurationManager: DefaultConfigurationManager { } private func updateTrackerBlockingDependencies() { - ContentBlocking.shared.privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration), - data: store.loadData(for: .privacyConfiguration)) - ContentBlocking.shared.trackerDataManager.reload(etag: store.loadEtag(for: .trackerDataSet), - data: store.loadData(for: .trackerDataSet)) + privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration), + data: store.loadData(for: .privacyConfiguration)) + trackerDataManager.reload(etag: store.loadEtag(for: .trackerDataSet), + data: store.loadData(for: .trackerDataSet)) NotificationCenter.default.post(name: ConfigurationManager.didUpdateTrackerDependencies, object: self) } diff --git a/DuckDuckGoTests/Configuration/AppConfigurationURLProviderTests.swift b/DuckDuckGoTests/Configuration/AppConfigurationURLProviderTests.swift new file mode 100644 index 0000000000..b2c1b2ce75 --- /dev/null +++ b/DuckDuckGoTests/Configuration/AppConfigurationURLProviderTests.swift @@ -0,0 +1,70 @@ +// +// AppConfigurationURLProviderTests.swift +// DuckDuckGo +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import BrowserServicesKit +import Configuration +@testable import DuckDuckGo + +final class AppConfigurationURLProviderTests: XCTestCase { + private var urlProvider: AppConfigurationURLProvider! + private var mockTdsURLProvider: MockTrackerDataURLProvider! + let controlURL = "control/url.json" + let treatmentURL = "treatment/url.json" + + override func setUp() { + super.setUp() + mockTdsURLProvider = MockTrackerDataURLProvider() + urlProvider = AppConfigurationURLProvider(trackerDataUrlProvider: mockTdsURLProvider) + } + + override func tearDown() { + urlProvider = nil + mockTdsURLProvider = nil + super.tearDown() + } + + func testUrlForTrackerDataIsDefaultWhenTdsUrlProviderUrlIsNil() { + // GIVEN + mockTdsURLProvider.trackerDataURL = nil + + // WHEN + let url = urlProvider.url(for: .trackerDataSet) + + // THEN + XCTAssertEqual(url, URL.trackerDataSet) + } + + func testUrlForTrackerDataIsTheOneProvidedByTdsUrlProvider() { + // GIVEN + let expectedURL = URL(string: "https://someurl.com")! + mockTdsURLProvider.trackerDataURL = expectedURL + + // WHEN + let url = urlProvider.url(for: .trackerDataSet) + + // THEN + XCTAssertEqual(url, expectedURL) + } + +} + +class MockTrackerDataURLProvider: TrackerDataURLProviding { + var trackerDataURL: URL? +} diff --git a/DuckDuckGoTests/Configuration/ConfigurationManagerTests.swift b/DuckDuckGoTests/Configuration/ConfigurationManagerTests.swift new file mode 100644 index 0000000000..392a925b92 --- /dev/null +++ b/DuckDuckGoTests/Configuration/ConfigurationManagerTests.swift @@ -0,0 +1,173 @@ +// +// ConfigurationManagerTests.swift +// DuckDuckGo +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Configuration +@testable import BrowserServicesKit +@testable import DuckDuckGo +import Combine +import TrackerRadarKit + +final class ConfigurationManagerTests: XCTestCase { + private var operationLog: OperationLog! + private var configManager: ConfigurationManager! + private var mockFetcher: MockConfigurationFetcher! + private var mockStore: MockConfigurationStoring! + private var mockTrackerDataManager: MockTrackerDataManager! + private var mockPrivacyConfigManager: MockPrivacyConfigurationManagerWithLogs! + + override func setUpWithError() throws { + operationLog = OperationLog() + let userDefaults = UserDefaults(suiteName: "ConfigurationManagerTests")! + userDefaults.removePersistentDomain(forName: "ConfigurationManagerTests") + mockFetcher = MockConfigurationFetcher(operationLog: operationLog) + mockStore = MockConfigurationStoring() + mockPrivacyConfigManager = MockPrivacyConfigurationManagerWithLogs(operationLog: operationLog, fetchedETag: nil, fetchedData: nil, embeddedDataProvider: MockEmbeddedDataProvider(data: Data(), etag: "etag"), localProtection: MockDomainsProtectionStore(), internalUserDecider: DefaultInternalUserDecider()) + mockPrivacyConfigManager.operationLog = operationLog + mockTrackerDataManager = MockTrackerDataManager(operationLog: operationLog, etag: nil, data: nil, embeddedDataProvider: MockEmbeddedDataProvider(data: Data(), etag: "etag")) + configManager = ConfigurationManager(fetcher: mockFetcher, + store: mockStore, + defaults: userDefaults, + trackerDataManager: mockTrackerDataManager, + privacyConfigurationManager: mockPrivacyConfigManager) + } + + override func tearDownWithError() throws { + operationLog = nil + configManager = nil + mockStore = nil + mockFetcher = nil + mockTrackerDataManager = nil + mockPrivacyConfigManager = nil + } + + func test_WhenRefreshNow_AndPrivacyConfigFetchFails_OtherFetchStillHappen() async { + // GIVEN + mockFetcher.shouldFailPrivacyFetch = true + operationLog.steps = [] + let expectedOrder: [ConfigurationStep] = [ + .fetchPrivacyConfigStarted, + .fetchSurrogatesStarted, + .fetchTrackerDataSetStarted, + .reloadPrivacyConfig, + .reloadTrackerDataSet + ] + + // WHEN + await configManager.fetchAndUpdateTrackerBlockingDependencies() + + // THEN + XCTAssertEqual(operationLog.steps, expectedOrder, "Operations did not occur in the expected order.") + } + + func test_WhenRefreshNow_ThenPrivacyConfigFetchAndReloadBeforeTrackerDataSetFetch() async { + // GIVEN + operationLog.steps = [] + let expectedOrder: [ConfigurationStep] = [ + .fetchPrivacyConfigStarted, + .fetchSurrogatesStarted, + .reloadPrivacyConfig, + .fetchTrackerDataSetStarted, + .reloadPrivacyConfig, + .reloadTrackerDataSet + ] + + // WHEN + await configManager.fetchAndUpdateTrackerBlockingDependencies() + + // THEN + XCTAssertEqual(operationLog.steps, expectedOrder, "Operations did not occur in the expected order.") + } + +} + +// Step enum to track operations +private enum ConfigurationStep: String, Equatable { + case fetchSurrogatesStarted + case fetchPrivacyConfigStarted + case fetchTrackerDataSetStarted + case reloadPrivacyConfig + case reloadTrackerDataSet +} + +private class MockConfigurationFetcher: ConfigurationFetching { + var operationLog: OperationLog + var shouldFailPrivacyFetch = false + + init(operationLog: OperationLog) { + self.operationLog = operationLog + } + + func fetch(_ configuration: Configuration, isDebug: Bool) async throws { + switch configuration { + case .bloomFilterBinary: + break + case .bloomFilterSpec: + break + case .bloomFilterExcludedDomains: + break + case .privacyConfiguration: + operationLog.steps.append(.fetchPrivacyConfigStarted) + if shouldFailPrivacyFetch { + throw NSError(domain: "TestError", code: 1, userInfo: nil) + } + try await Task.sleep(nanoseconds: 50_000_000) + case .surrogates: + operationLog.steps.append(.fetchSurrogatesStarted) + case .trackerDataSet: + operationLog.steps.append(.fetchTrackerDataSetStarted) + case .remoteMessagingConfig: + break + } + } + + func fetch(all configurations: [Configuration]) async throws {} +} + +private class MockPrivacyConfigurationManagerWithLogs: PrivacyConfigurationManager { + var operationLog: OperationLog + + init(operationLog: OperationLog, fetchedETag: String?, fetchedData: Data?, embeddedDataProvider: any EmbeddedDataProvider, localProtection: any DomainsProtectionStore, internalUserDecider: any InternalUserDecider) { + self.operationLog = operationLog + super.init(fetchedETag: fetchedETag, fetchedData: fetchedData, embeddedDataProvider: embeddedDataProvider, localProtection: localProtection, internalUserDecider: internalUserDecider) + } + + override func reload(etag: String?, data: Data?) -> ReloadResult { + operationLog.steps.append(.reloadPrivacyConfig) + return .embedded + } +} + +private class MockTrackerDataManager: TrackerDataManager { + var operationLog: OperationLog + + init(operationLog: OperationLog, etag: String?, data: Data?, embeddedDataProvider: any EmbeddedDataProvider) { + self.operationLog = operationLog + super.init(etag: etag, data: data, embeddedDataProvider: embeddedDataProvider) + } + + public override func reload(etag: String?, data: Data?) -> ReloadResult { + operationLog.steps.append(.reloadTrackerDataSet) + return .embedded + } +} + +private class OperationLog { + var steps: [ConfigurationStep] = [] +} diff --git a/DuckDuckGoTests/MockPrivacyConfiguration.swift b/DuckDuckGoTests/MockPrivacyConfiguration.swift index 07c69f3679..dfd3f25e05 100644 --- a/DuckDuckGoTests/MockPrivacyConfiguration.swift +++ b/DuckDuckGoTests/MockPrivacyConfiguration.swift @@ -56,6 +56,11 @@ class MockPrivacyConfiguration: PrivacyConfiguration { var exceptionsList: (PrivacyFeature) -> [String] = { _ in [] } var featureSettings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings = [:] + var subfeatureSettings: String? + func settings(for subfeature: any PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? { + return subfeatureSettings + } + func exceptionsList(forFeature featureKey: PrivacyFeature) -> [String] { exceptionsList(featureKey) } var isFeatureKeyEnabled: ((PrivacyFeature, AppVersionProvider) -> Bool)? func isEnabled(featureKey: PrivacyFeature, versionProvider: AppVersionProvider) -> Bool { diff --git a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift index 1f3128ec92..c4bb43f888 100644 --- a/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift +++ b/DuckDuckGoTests/PrivacyConfigurationManagerMock.swift @@ -98,6 +98,10 @@ class PrivacyConfigurationMock: PrivacyConfiguration { return settings[feature] ?? [:] } + func settings(for subfeature: any BrowserServicesKit.PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? { + return nil + } + var userUnprotected = Set() func userEnabledProtection(forDomain domain: String) { userUnprotected.remove(domain) diff --git a/IntegrationTests/Configuration/ConfigurationManagerIntegrationTests.swift b/IntegrationTests/Configuration/ConfigurationManagerIntegrationTests.swift new file mode 100644 index 0000000000..dd8fca2118 --- /dev/null +++ b/IntegrationTests/Configuration/ConfigurationManagerIntegrationTests.swift @@ -0,0 +1,69 @@ +// +// ConfigurationManagerIntegrationTests.swift +// DuckDuckGo +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Core +import Configuration +@testable import DuckDuckGo + +final class ConfigurationManagerIntegrationTests: XCTestCase { + + var configManager: ConfigurationManager! + var customURLProvider: CustomConfigurationURLProvider! + + override func setUpWithError() throws { + // use default privacyConfiguration link + customURLProvider = CustomConfigurationURLProvider() + customURLProvider.customPrivacyConfigurationURL = URL.privacyConfig + Configuration.setURLProvider(customURLProvider) + configManager = ConfigurationManager() + } + + override func tearDownWithError() throws { + // use default privacyConfiguration link + customURLProvider.customPrivacyConfigurationURL = URL.privacyConfig + Configuration.setURLProvider(customURLProvider) + configManager = nil + } + + func testTdsAreFetchedFromURLBasedOnPrivacyConfigExperiment() async { + // GIVEN + await configManager.fetchAndUpdateTrackerBlockingDependencies() + let etag = ContentBlocking.shared.trackerDataManager.fetchedData?.etag + // use test privacyConfiguration link with tds experiments + customURLProvider.customPrivacyConfigurationURL = URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/config/test/macos-config.json")! + Configuration.setURLProvider(customURLProvider) + + // WHEN + await configManager.fetchAndUpdateTrackerBlockingDependencies() + + // THEN + var newEtag = ContentBlocking.shared.trackerDataManager.fetchedData?.etag + XCTAssertNotEqual(etag, newEtag) + XCTAssertEqual(newEtag, "\"2ce60c57c3d384f986ccbe2c422aac44\"") + + // RESET + customURLProvider.customPrivacyConfigurationURL = URL.privacyConfig + Configuration.setURLProvider(customURLProvider) + await configManager.fetchAndUpdateTrackerBlockingDependencies() + newEtag = ContentBlocking.shared.trackerDataManager.fetchedData?.etag + XCTAssertEqual(etag, newEtag) + } + +}