From 4f1655b163e1cdc17077b826b4998122f582122e Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Fri, 17 Jan 2025 15:48:39 +0100 Subject: [PATCH] tds override (#3726) Task/Issue URL: https://app.asana.com/0/1204186595873227/1208547600159200/f Tech Design URL: https://app.asana.com/0/481882893211075/1208547600159199 CC: **Description**: Makes the tds fetch dependent on the privacy configuration fetch (so that the url for tds can be taken from the configuration). Sets up experiment for tds. Note metrics will be added in a separate PR. --- DBPE2ETests/DBPEndToEndTests.swift | 4 + DuckDuckGo.xcodeproj/project.pbxproj | 22 +- .../xcshareddata/swiftpm/Package.resolved | 12 +- .../AppConfigurationURLProvider.swift | 31 ++- DuckDuckGo/Application/AppDelegate.swift | 4 +- .../Configuration/ConfigurationManager.swift | 57 ++++- .../Mocks/MockPrivacyConfiguration.swift | 4 + ...ConfigurationManagerIntegrationTests.swift | 60 +++++ .../DataBrokerProtection/Package.swift | 2 +- .../DataBrokerProtectionTests/Mocks.swift | 4 + LocalPackages/FeatureFlags/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/NewTabPage/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- LocalPackages/WebKitExtensions/Package.swift | 2 +- .../AppConfigurationURLProviderTests.swift | 45 ++++ .../ConfigurationManagerTests.swift | 205 ++++++++++++++++++ .../ErrorPageTabExtensionTest.swift | 3 +- 18 files changed, 431 insertions(+), 32 deletions(-) create mode 100644 IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift create mode 100644 UnitTests/Configuration/ConfigurationManagerTests.swift diff --git a/DBPE2ETests/DBPEndToEndTests.swift b/DBPE2ETests/DBPEndToEndTests.swift index e9c3aa5cb0..72624ec1d7 100644 --- a/DBPE2ETests/DBPEndToEndTests.swift +++ b/DBPE2ETests/DBPEndToEndTests.swift @@ -496,6 +496,10 @@ private extension DBPEndToEndTests { [String: Any]() } + func settings(for subfeature: any BrowserServicesKit.PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? { + nil + } + func userEnabledProtection(forDomain: String) { } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 84672f2d10..06a5ccb48f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1682,6 +1682,8 @@ 562532A12BC069190034D316 /* ZoomPopoverViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5625329D2BC069100034D316 /* ZoomPopoverViewModelTests.swift */; }; 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; 562984712AC469E400AC20EB /* SyncPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */; }; + 563A3CFA2D37AE2A001966FD /* ConfigurationManagerIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563A3CF92D37AE2A001966FD /* ConfigurationManagerIntegrationTests.swift */; }; + 563A3CFB2D37AE2A001966FD /* ConfigurationManagerIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 563A3CF92D37AE2A001966FD /* ConfigurationManagerIntegrationTests.swift */; }; 56406D4B2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56406D4A2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift */; }; 56406D4C2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56406D4A2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift */; }; 5641734B2CFE168700F4B716 /* PixelExperimentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5641734A2CFE168700F4B716 /* PixelExperimentKit */; }; @@ -1775,6 +1777,8 @@ 56BA1E802BAB2E43001CF69F /* ErrorPageTabExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */; }; 56BA1E8A2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; 56BA1E8B2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; + 56BC8F012D312B320046059D /* ConfigurationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BC8F002D312B320046059D /* ConfigurationManagerTests.swift */; }; + 56BC8F022D312B320046059D /* ConfigurationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BC8F002D312B320046059D /* ConfigurationManagerTests.swift */; }; 56CE77612C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */; }; 56CE77622C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */; }; 56CEE90E2B7A725B00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; @@ -4085,6 +4089,7 @@ 561D66692B95C45A008ACC5C /* Suggestion.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Suggestion.storyboard; sourceTree = ""; }; 5625329D2BC069100034D316 /* ZoomPopoverViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomPopoverViewModelTests.swift; sourceTree = ""; }; 5629846E2AC4610100AC20EB /* SyncPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferencesTests.swift; sourceTree = ""; }; + 563A3CF92D37AE2A001966FD /* ConfigurationManagerIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManagerIntegrationTests.swift; sourceTree = ""; }; 56406D4A2C636A8900BF8FA2 /* SpecialPagesUserScriptExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialPagesUserScriptExtension.swift; sourceTree = ""; }; 56534DEC29DF252C00121467 /* CapturingDefaultBrowserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingDefaultBrowserProvider.swift; sourceTree = ""; }; 565E46DD2B2725DC0013AC2A /* SyncE2EUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SyncE2EUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -4131,6 +4136,7 @@ 56BA1E742BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageTabExtension.swift; sourceTree = ""; }; 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPageTabExtensionTest.swift; sourceTree = ""; }; 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateTrustEvaluator.swift; sourceTree = ""; }; + 56BC8F002D312B320046059D /* ConfigurationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManagerTests.swift; sourceTree = ""; }; 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProviderTests.swift; sourceTree = ""; }; 56CEE9092B7A66C500CF10AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; @@ -6234,6 +6240,7 @@ 4B1AD89E25FC27E200261379 /* IntegrationTests */ = { isa = PBXGroup; children = ( + 563A3CF82D37ADFA001966FD /* Configurations */, 560C6ECB2CCA5B9D00D411E2 /* Onboarding */, 84537A072C99C1EF008723BC /* App */, EEE0E1CB2C32F53C0058E148 /* DataImport */, @@ -7075,6 +7082,14 @@ path = Sync; sourceTree = ""; }; + 563A3CF82D37ADFA001966FD /* Configurations */ = { + isa = PBXGroup; + children = ( + 563A3CF92D37AE2A001966FD /* ConfigurationManagerIntegrationTests.swift */, + ); + path = Configurations; + sourceTree = ""; + }; 56534DEB29DF251C00121467 /* Mocks */ = { isa = PBXGroup; children = ( @@ -7496,6 +7511,7 @@ isa = PBXGroup; children = ( 85AC3B4825DAC9BD00C7D2AA /* ConfigurationStorageTests.swift */, + 56BC8F002D312B320046059D /* ConfigurationManagerTests.swift */, ); path = Configuration; sourceTree = ""; @@ -12454,6 +12470,7 @@ 3706FE2F293F661700E42796 /* WebViewMock.swift in Sources */, 3706FE30293F661700E42796 /* CollectionExtension.swift in Sources */, B630E80129C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, + 56BC8F012D312B320046059D /* ConfigurationManagerTests.swift in Sources */, 3706FE31293F661700E42796 /* TabCollectionViewModelDelegateMock.swift in Sources */, 3706FE32293F661700E42796 /* BookmarksHTMLReaderTests.swift in Sources */, 9F0FFFBC2BCCAEC2007C87DD /* AddEditBookmarkFolderDialogViewModelMock.swift in Sources */, @@ -12670,6 +12687,7 @@ 3706FEA6293F662100E42796 /* EncryptionKeyStoreTests.swift in Sources */, B6F5656A299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, EEE0E1CD2C32F5690058E148 /* CSVImporterIntegrationTests.swift in Sources */, + 563A3CFB2D37AE2A001966FD /* ConfigurationManagerIntegrationTests.swift in Sources */, B693766F2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */, 56A054362C205820007D8FAB /* OnboardingPageTests.swift in Sources */, ); @@ -12719,6 +12737,7 @@ 4B1AD8D525FC38DD00261379 /* EncryptionKeyStoreTests.swift in Sources */, B6F56568299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, EEE0E1D12C32F8620058E148 /* CSVImporterIntegrationTests.swift in Sources */, + 563A3CFA2D37AE2A001966FD /* ConfigurationManagerIntegrationTests.swift in Sources */, B693766E2B6B5F27005BD9D4 /* ErrorPageTests.swift in Sources */, 56A054352C20581F007D8FAB /* OnboardingPageTests.swift in Sources */, ); @@ -14293,6 +14312,7 @@ B6CA4824298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */, AAEC74B22642C57200C2EFBC /* HistoryCoordinatingMock.swift in Sources */, 37D046A12C7DA9A200AEAA50 /* UserBackgroundImagesManagerTests.swift in Sources */, + 56BC8F022D312B320046059D /* ConfigurationManagerTests.swift in Sources */, 56A214AF2CB583BF00E5BC0E /* TrackerMessageProviderTests.swift in Sources */, 37CD54B927F1F8AC00F1F7B9 /* AppearancePreferencesTests.swift in Sources */, EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */, @@ -15366,7 +15386,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 224.7.2; + version = 225.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 598affb3be..fb5f5537b3 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" } }, { @@ -167,8 +167,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/Application/AppConfigurationURLProvider.swift b/DuckDuckGo/Application/AppConfigurationURLProvider.swift index 188a95d698..f27b87057e 100644 --- a/DuckDuckGo/Application/AppConfigurationURLProvider.swift +++ b/DuckDuckGo/Application/AppConfigurationURLProvider.swift @@ -18,12 +18,17 @@ import Configuration import Foundation +import BrowserServicesKit +import os.log struct AppConfigurationURLProvider: ConfigurationURLProviding { // MARK: - Debug - - internal init(customPrivacyConfiguration: URL? = nil) { + internal init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + featureFlagger: FeatureFlagger = Application.appDelegate.featureFlagger, + customPrivacyConfiguration: URL? = nil) { + let trackerDataUrlProvider = TrackerDataURLOverrider(privacyConfigurationManager: privacyConfigurationManager, featureFlagger: featureFlagger) + self.init(trackerDataUrlProvider: trackerDataUrlProvider) if let customPrivacyConfiguration { // Overwrite custom privacy configuration if provided self.customPrivacyConfiguration = customPrivacyConfiguration.absoluteString @@ -47,6 +52,23 @@ struct AppConfigurationURLProvider: ConfigurationURLProviding { // MARK: - Main + private var trackerDataUrlProvider: TrackerDataURLProviding + + public enum Constants { + public static let baseTdsURLString = "https://staticcdn.duckduckgo.com/trackerblocking/" + public static let defaultTrackerDataURL = URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/v6/current/macos-tds.json")! + public static let defaultPrivacyConfigurationURL = URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/config/v4/macos-config.json")! + } + + init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + featureFlagger: FeatureFlagger = Application.appDelegate.featureFlagger) { + self.trackerDataUrlProvider = TrackerDataURLOverrider(privacyConfigurationManager: privacyConfigurationManager, featureFlagger: featureFlagger) + } + + init(trackerDataUrlProvider: TrackerDataURLProviding) { + self.trackerDataUrlProvider = trackerDataUrlProvider + } + func url(for configuration: Configuration) -> URL { // URLs for privacyConfiguration and trackerDataSet shall match the ones in update_embedded.sh. // Danger checks that the URLs match on every PR. If the code changes, the regex that Danger uses may need an update. @@ -54,9 +76,10 @@ struct AppConfigurationURLProvider: ConfigurationURLProviding { case .bloomFilterBinary: return URL(string: "https://staticcdn.duckduckgo.com/https/https-mobile-v2-bloom.bin")! case .bloomFilterSpec: return URL(string: "https://staticcdn.duckduckgo.com/https/https-mobile-v2-bloom-spec.json")! case .bloomFilterExcludedDomains: return URL(string: "https://staticcdn.duckduckgo.com/https/https-mobile-v2-false-positives.json")! - case .privacyConfiguration: return customPrivacyConfigurationUrl ?? URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/config/v4/macos-config.json")! + case .privacyConfiguration: return customPrivacyConfigurationUrl ?? Constants.defaultPrivacyConfigurationURL case .surrogates: return URL(string: "https://staticcdn.duckduckgo.com/surrogates.txt")! - case .trackerDataSet: return URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/v6/current/macos-tds.json")! + case .trackerDataSet: + return trackerDataUrlProvider.trackerDataURL ?? Constants.defaultTrackerDataURL // In archived repo, to be refactored shortly (https://staticcdn.duckduckgo.com/useragents/social_ctp_configuration.json) case .remoteMessagingConfig: return RemoteMessagingClient.Constants.endpoint } diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 3aa256fb0f..46428b6482 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -198,8 +198,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore) internalUserDecider = DefaultInternalUserDecider(store: internalUserDeciderStore) - configurationManager = ConfigurationManager(store: configurationStore) - if NSApplication.runType.requiresEnvironment { Self.configurePixelKit() @@ -279,6 +277,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { ) } + configurationManager = ConfigurationManager(store: configurationStore) + featureFlagger = DefaultFeatureFlagger( internalUserDecider: internalUserDecider, privacyConfigManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index f193bb836a..5fec8cd9b6 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -28,6 +28,10 @@ import PixelKit final class ConfigurationManager: DefaultConfigurationManager { + private let trackerDataManager: TrackerDataManager + private let privacyConfigurationManager: PrivacyConfigurationManaging + private var contentBlockingManager: ContentBlockerRulesManagerProtocol + private enum Constants { static let lastConfigurationInstallDateKey = "config.last.installed" } @@ -53,10 +57,18 @@ final class ConfigurationManager: DefaultConfigurationManager { PixelKit.fire(DebugEvent(domainEvent, error: error)) } - override init(fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents), - store: ConfigurationStoring = ConfigurationStore(), - defaults: KeyValueStoring = UserDefaults.appConfiguration) { + init(fetcher: ConfigurationFetching = ConfigurationFetcher(store: ConfigurationStore(), eventMapping: configurationDebugEvents), + store: ConfigurationStoring = ConfigurationStore(), + defaults: KeyValueStoring = UserDefaults.appConfiguration, + trackerDataManager: TrackerDataManager = ContentBlocking.shared.trackerDataManager, + privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, + contentBlockingManager: ContentBlockerRulesManagerProtocol = ContentBlocking.shared.contentBlockingManager) { + + self.trackerDataManager = trackerDataManager + self.privacyConfigurationManager = privacyConfigurationManager + self.contentBlockingManager = contentBlockingManager self.defaults = defaults + super.init(fetcher: fetcher, store: store, defaults: defaults) } @@ -107,10 +119,30 @@ final class ConfigurationManager: DefaultConfigurationManager { private func fetchTrackerBlockingDependencies(isDebug: Bool) 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.config.error( + "Failed to complete configuration update to \(Configuration.privacyConfiguration.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)" + ) + tryAgainSoon() + } + + // 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 { @@ -141,11 +173,12 @@ final class ConfigurationManager: DefaultConfigurationManager { private func updateTrackerBlockingDependencies() { lastConfigurationInstallDate = Date() - ContentBlocking.shared.trackerDataManager.reload(etag: store.loadEtag(for: .trackerDataSet), - data: store.loadData(for: .trackerDataSet)) - ContentBlocking.shared.privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration), - data: store.loadData(for: .privacyConfiguration)) - ContentBlocking.shared.contentBlockingManager.scheduleCompilation() + + trackerDataManager.reload(etag: store.loadEtag(for: .trackerDataSet), + data: store.loadData(for: .trackerDataSet)) + privacyConfigurationManager.reload(etag: store.loadEtag(for: .privacyConfiguration), + data: store.loadData(for: .privacyConfiguration)) + contentBlockingManager.scheduleCompilation() } private func updateBloomFilter() async throws { diff --git a/DuckDuckGo/ContentBlocker/Mocks/MockPrivacyConfiguration.swift b/DuckDuckGo/ContentBlocker/Mocks/MockPrivacyConfiguration.swift index a88fc18db9..0cb08b6d40 100644 --- a/DuckDuckGo/ContentBlocker/Mocks/MockPrivacyConfiguration.swift +++ b/DuckDuckGo/ContentBlocker/Mocks/MockPrivacyConfiguration.swift @@ -44,6 +44,7 @@ final class MockPrivacyConfiguration: PrivacyConfiguration { state: PrivacyConfigurationData.State.enabled) var exceptionsList: (PrivacyFeature) -> [String] = { _ in [] } var featureSettings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings = [:] + var subfeatureSettings: PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings = "" func exceptionsList(forFeature featureKey: PrivacyFeature) -> [String] { exceptionsList(featureKey) } var isFeatureKeyEnabled: ((PrivacyFeature, AppVersionProvider) -> Bool)? @@ -76,6 +77,9 @@ final class MockPrivacyConfiguration: PrivacyConfiguration { func isInExceptionList(domain: String?, forFeature featureKey: PrivacyFeature) -> Bool { false } func settings(for feature: PrivacyFeature) -> PrivacyConfigurationData.PrivacyFeature.FeatureSettings { featureSettings } + func settings(for subfeature: any BrowserServicesKit.PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? { + subfeatureSettings + } func userEnabledProtection(forDomain: String) {} func userDisabledProtection(forDomain: String) {} } diff --git a/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift b/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift new file mode 100644 index 0000000000..c3c213057f --- /dev/null +++ b/IntegrationTests/Configurations/ConfigurationManagerIntegrationTests.swift @@ -0,0 +1,60 @@ +// +// ConfigurationManagerIntegrationTests.swift +// +// 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 +@testable import DuckDuckGo_Privacy_Browser + +final class ConfigurationManagerIntegrationTests: XCTestCase { + + var configManager: ConfigurationManager! + + override func setUpWithError() throws { + // use default privacyConfiguration link + _ = AppConfigurationURLProvider(customPrivacyConfiguration: AppConfigurationURLProvider.Constants.defaultPrivacyConfigurationURL) + configManager = ConfigurationManager() + } + + override func tearDownWithError() throws { + // use default privacyConfiguration link + _ = AppConfigurationURLProvider(customPrivacyConfiguration: AppConfigurationURLProvider.Constants.defaultPrivacyConfigurationURL) + configManager = nil + } + + func testTdsAreFetchedFromURLBasedOnPrivacyConfigExperiment() async { + // GIVEN + await configManager.refreshNow() + let etag = ContentBlocking.shared.trackerDataManager.fetchedData?.etag + // use test privacyConfiguration link with tds experiments + _ = AppConfigurationURLProvider(customPrivacyConfiguration: URL(string: "https://staticcdn.duckduckgo.com/trackerblocking/config/test/macos-config.json")!) + + // WHEN + await configManager.refreshNow() + + // THEN + var newEtag = ContentBlocking.shared.trackerDataManager.fetchedData?.etag + XCTAssertNotEqual(etag, newEtag) + XCTAssertEqual(newEtag, "\"2ce60c57c3d384f986ccbe2c422aac44\"") + + // RESET + _ = AppConfigurationURLProvider(customPrivacyConfiguration: AppConfigurationURLProvider.Constants.defaultPrivacyConfigurationURL) + await configManager.refreshNow() + newEtag = ContentBlocking.shared.trackerDataManager.fetchedData?.etag + XCTAssertEqual(etag, newEtag) + } + +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 5d367ed425..bdb4a5fec8 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "224.7.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "225.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../AppKitExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index a1ee759fac..76747680a5 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -231,6 +231,10 @@ final class PrivacyConfigurationMock: PrivacyConfiguration { [String: Any]() } + func settings(for subfeature: any BrowserServicesKit.PrivacySubfeature) -> PrivacyConfigurationData.PrivacyFeature.SubfeatureSettings? { + return nil + } + func userEnabledProtection(forDomain: String) { } diff --git a/LocalPackages/FeatureFlags/Package.swift b/LocalPackages/FeatureFlags/Package.swift index 36e716343e..960f4c7a76 100644 --- a/LocalPackages/FeatureFlags/Package.swift +++ b/LocalPackages/FeatureFlags/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["FeatureFlags"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "224.7.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "225.0.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index b50f73d854..ef83a3965f 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -33,7 +33,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "224.7.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "225.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/NewTabPage/Package.swift b/LocalPackages/NewTabPage/Package.swift index d67d938f8e..68a041c2a5 100644 --- a/LocalPackages/NewTabPage/Package.swift +++ b/LocalPackages/NewTabPage/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["NewTabPage"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "224.7.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "225.0.0"), .package(path: "../WebKitExtensions"), .package(path: "../Utilities"), ], diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 4bfad86520..0848795ec1 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -13,7 +13,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "224.7.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "225.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../FeatureFlags") ], diff --git a/LocalPackages/WebKitExtensions/Package.swift b/LocalPackages/WebKitExtensions/Package.swift index f8911234ae..38502ce36a 100644 --- a/LocalPackages/WebKitExtensions/Package.swift +++ b/LocalPackages/WebKitExtensions/Package.swift @@ -32,7 +32,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "224.7.2"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "225.0.0"), .package(path: "../AppKitExtensions") ], targets: [ diff --git a/UnitTests/AppDelegate/AppConfigurationURLProviderTests.swift b/UnitTests/AppDelegate/AppConfigurationURLProviderTests.swift index 2fc10c8ce0..7c67c79557 100644 --- a/UnitTests/AppDelegate/AppConfigurationURLProviderTests.swift +++ b/UnitTests/AppDelegate/AppConfigurationURLProviderTests.swift @@ -17,9 +17,27 @@ // import XCTest +import BrowserServicesKit +import Configuration @testable import DuckDuckGo_Privacy_Browser 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 testExternalURLDependenciesAreExpected() throws { XCTAssertEqual(AppConfigurationURLProvider().url(for: .bloomFilterBinary).absoluteString, "https://staticcdn.duckduckgo.com/https/https-mobile-v2-bloom.bin") @@ -30,4 +48,31 @@ final class AppConfigurationURLProviderTests: XCTestCase { XCTAssertEqual(AppConfigurationURLProvider().url(for: .trackerDataSet).absoluteString, "https://staticcdn.duckduckgo.com/trackerblocking/v6/current/macos-tds.json") } + func testUrlForTrackerDataIsDefaultWhenTdsUrlProviderUrlIsNil() { + // GIVEN + mockTdsURLProvider.trackerDataURL = nil + + // WHEN + let url = urlProvider.url(for: .trackerDataSet) + + // THEN + XCTAssertEqual(url, AppConfigurationURLProvider.Constants.defaultTrackerDataURL) + } + + 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/UnitTests/Configuration/ConfigurationManagerTests.swift b/UnitTests/Configuration/ConfigurationManagerTests.swift new file mode 100644 index 0000000000..807773307f --- /dev/null +++ b/UnitTests/Configuration/ConfigurationManagerTests.swift @@ -0,0 +1,205 @@ +// +// ConfigurationManagerTests.swift +// +// 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_Privacy_Browser +import Combine +import TrackerRadarKit + +final class ConfigurationManagerTests: XCTestCase { + private var operationLog: OperationLog! + private var configManager: ConfigurationManager! + private var mockFetcher: MockConfigurationFetcher! + private var mockStore: MockConfigurationStore! + private var mockTrackerDataManager: MockTrackerDataManager! + private var mockPrivacyConfigManager: MockPrivacyConfigurationManager! + private var mockContentBlockingManager: MockContentBlockerRulesManager! + + override func setUpWithError() throws { + operationLog = OperationLog() + let userDefaults = UserDefaults(suiteName: "ConfigurationManagerTests")! + userDefaults.removePersistentDomain(forName: "ConfigurationManagerTests") + mockFetcher = MockConfigurationFetcher(operationLog: operationLog) + mockStore = MockConfigurationStore() + mockPrivacyConfigManager = MockPrivacyConfigurationManager(operationLog: operationLog, fetchedETag: nil, fetchedData: nil, embeddedDataProvider: MockEmbeddedDataProvider(), localProtection: MockDomainsProtectionStore(), internalUserDecider: DefaultInternalUserDecider()) + mockPrivacyConfigManager.operationLog = operationLog + mockTrackerDataManager = MockTrackerDataManager(operationLog: operationLog, etag: nil, data: nil, embeddedDataProvider: MockEmbeddedDataProvider()) + mockContentBlockingManager = MockContentBlockerRulesManager(operationLog: operationLog) + configManager = ConfigurationManager(fetcher: mockFetcher, + store: mockStore, defaults: userDefaults, + trackerDataManager: mockTrackerDataManager, + privacyConfigurationManager: mockPrivacyConfigManager, + contentBlockingManager: mockContentBlockingManager) + } + + override func tearDownWithError() throws { + operationLog = nil + configManager = nil + mockStore = nil + mockFetcher = nil + mockTrackerDataManager = nil + mockPrivacyConfigManager = nil + mockContentBlockingManager = nil + } + + func test_WhenRefreshNow_AndPrivacyConfigFetchFails_OtherFetchStillHappen() async { + // GIVEN + mockFetcher.shouldFailPrivacyFetch = true + operationLog.steps = [] + let expectedFirstTwo: Set = [.fetchPrivacyConfigStarted, .fetchSurrogatesStarted] + let expectedOrder: [ConfigurationStep] = [ + .fetchTrackerDataSetStarted, + .reloadTrackerDataSet, + .reloadPrivacyConfig, + .contentBlockingScheduleCompilation + ] + + // WHEN + await configManager.refreshNow(isDebug: false) + + // THEN + XCTAssertEqual(Set(operationLog.steps.prefix(2)), expectedFirstTwo, "Steps do not match the expected order.") + XCTAssertEqual(Array(operationLog.steps.dropFirst(2)), expectedOrder, "Steps do not match the expected order.") + } + + func test_WhenRefreshNow_ThenPrivacyConfigFetchAndReloadBeforeTrackerDataSetFetch() async { + // GIVEN + operationLog.steps = [] + let expectedFirstTwo: Set = [.fetchPrivacyConfigStarted, .fetchSurrogatesStarted] + let expectedOrder: [ConfigurationStep] = [ + .reloadPrivacyConfig, + .fetchTrackerDataSetStarted, + .reloadTrackerDataSet, + .reloadPrivacyConfig, + .contentBlockingScheduleCompilation + ] + + // WHEN + await configManager.refreshNow(isDebug: false) + + // THEN + XCTAssertEqual(Set(operationLog.steps.prefix(2)), expectedFirstTwo, "Steps do not match the expected order.") + XCTAssertEqual(Array(operationLog.steps.dropFirst(2)), expectedOrder, "Steps do not match the expected order.") + } + +} + +// Step enum to track operations +private enum ConfigurationStep: String, Equatable { + case fetchSurrogatesStarted + case fetchPrivacyConfigStarted + case fetchTrackerDataSetStarted + case reloadPrivacyConfig + case reloadTrackerDataSet + case contentBlockingScheduleCompilation +} + +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 MockPrivacyConfigurationManager: 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 MockContentBlockerRulesManager: ContentBlockerRulesManagerProtocol { + var operationLog: OperationLog + + init(operationLog: OperationLog) { + self.operationLog = operationLog + } + + var updatesPublisher: AnyPublisher = Empty().eraseToAnyPublisher() + + var currentRules: [ContentBlockerRulesManager.Rules] = [] + + func scheduleCompilation() -> ContentBlockerRulesManager.CompletionToken { + operationLog.steps.append(.contentBlockingScheduleCompilation) + return "" + } + + var currentMainRules: ContentBlockerRulesManager.Rules? + + var currentAttributionRules: BrowserServicesKit.ContentBlockerRulesManager.Rules? + + func entity(forHost host: String) -> Entity? { + return nil + } + + func scheduleCompilation() {} +} + +private class OperationLog { + var steps: [ConfigurationStep] = [] +} diff --git a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift index ba008ddd73..885da505ae 100644 --- a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift +++ b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift @@ -540,6 +540,7 @@ class ChallangeSender: URLAuthenticationChallengeSender { class MockFeatureFlagger: FeatureFlagger { var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: MockInternalUserStoring()) var localOverrides: FeatureFlagLocalOverriding? + var cohort: (any FlagCohort)? var isFeatureOn = true func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { @@ -551,7 +552,7 @@ class MockFeatureFlagger: FeatureFlagger { } func getCohortIfEnabled(for featureFlag: Flag) -> (any FlagCohort)? where Flag: FeatureFlagExperimentDescribing { - return nil + return cohort } func getAllActiveExperiments() -> Experiments {