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 {