diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7d447f74b1..ddff21e5e7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -206,6 +206,12 @@ 31E163BA293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */; }; 31E163BD293A579E00963C10 /* PrivacyReferenceTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */; }; 31E163C0293A581900963C10 /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = 31E163BF293A581900963C10 /* privacy-reference-tests */; }; + 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; }; + 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; + 31ECDA122BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; + 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; + 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */; }; 31EF1E802B63FFA800E6DB17 /* DBPHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3192EC872A4DCF21001E97A5 /* DBPHomeViewController.swift */; }; 31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; }; 31EF1E822B63FFC200E6DB17 /* DataBrokerProtectionLoginItemScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6D98662ADEB4B000CD35FE /* DataBrokerProtectionLoginItemScheduler.swift */; }; @@ -2907,6 +2913,7 @@ 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSiteReportingReferenceTests.swift; sourceTree = ""; }; 31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyReferenceTestHelper.swift; sourceTree = ""; }; 31E163BF293A581900963C10 /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "Submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; }; + 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerAuthenticationManagerBuilder.swift; sourceTree = ""; }; 31F28C4C28C8EEC500119F70 /* YoutubePlayerUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubePlayerUserScript.swift; sourceTree = ""; }; 31F28C4E28C8EEC500119F70 /* YoutubeOverlayUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubeOverlayUserScript.swift; sourceTree = ""; }; 31F28C5228C8EECA00119F70 /* DuckURLSchemeHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckURLSchemeHandler.swift; sourceTree = ""; }; @@ -6322,6 +6329,7 @@ 7B0099782B65013800FE7C31 /* BrowserWindowManager.swift */, 9D9AE92B2AAB84FF0026E7DC /* DBPMocks.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, + 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, 9D9AE91A2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgent.entitlements */, 9D9AE9192AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppStore.entitlements */, @@ -10276,6 +10284,7 @@ B602E8172A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 3706FCA1293F65D500E42796 /* AdjacentItemEnumerator.swift in Sources */, 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, + 31ECDA122BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 3706FCA2293F65D500E42796 /* ChromiumKeychainPrompt.swift in Sources */, 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */, 3706FCA3293F65D500E42796 /* WKProcessPool+GeolocationProvider.swift in Sources */, @@ -10842,6 +10851,8 @@ 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, 7BD01C192AD8319C0088B32E /* IPCServiceManager.swift in Sources */, + 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, + 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10856,6 +10867,8 @@ 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, 315A023D2B64216B00BFA577 /* IPCServiceManager.swift in Sources */, + 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, + 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -11156,6 +11169,7 @@ B60293E62BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, B65E6BA026D9F10600095F96 /* NSBezierPathExtension.swift in Sources */, + 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, diff --git a/DuckDuckGo/DBP/DBPHomeViewController.swift b/DuckDuckGo/DBP/DBPHomeViewController.swift index cf9e05e900..d6ececa0bb 100644 --- a/DuckDuckGo/DBP/DBPHomeViewController.swift +++ b/DuckDuckGo/DBP/DBPHomeViewController.swift @@ -105,8 +105,9 @@ final class DBPHomeViewController: NSViewController { override func viewDidAppear() { super.viewDidAppear() - if shouldAskForInviteCode() { - presentInviteCodeFlow() + if !dataBrokerProtectionManager.isUserAuthenticated() { + assertionFailure("This UI should never be presented if the user is not authenticated") + closeUI() } } @@ -118,9 +119,7 @@ final class DBPHomeViewController: NSViewController { } private func setupUI() { - if !shouldAskForInviteCode() { - setupUIWithCurrentStatus() - } + setupUIWithCurrentStatus() } private func setupObserver() { @@ -163,10 +162,6 @@ final class DBPHomeViewController: NSViewController { } } - private func shouldAskForInviteCode() -> Bool { - prerequisiteVerifier.checkStatus() == .valid && dataBrokerProtectionManager.shouldAskForInviteCode() - } - private func displayDBPUI() { replaceChildController(dataBrokerProtectionViewController) } @@ -185,6 +180,12 @@ final class DBPHomeViewController: NSViewController { NotificationCenter.default.removeObserver(observer) } } + + private func closeUI() { + presentedWindowController?.window?.close() + presentedWindowController = nil + NotificationCenter.default.post(name: .dbpDidClose, object: nil) + } } extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDelegate { @@ -195,9 +196,7 @@ extension DBPHomeViewController: DataBrokerProtectionInviteDialogsViewModelDeleg } func dataBrokerProtectionInviteDialogsViewModelDidCancel(_ viewModel: DataBrokerProtectionInviteDialogsViewModel) { - presentedWindowController?.window?.close() - presentedWindowController = nil - NotificationCenter.default.post(name: .dbpDidClose, object: nil) + closeUI() } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index b22ee8eac2..a552bf4bc6 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -316,7 +316,8 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func runCustomJSON() { - let viewController = DataBrokerRunCustomJSONViewController() + let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager() + let viewController = DataBrokerRunCustomJSONViewController(authenticationManager: authenticationManager) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index f3b8705674..9ecde48a3e 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -29,9 +29,7 @@ public final class DataBrokerProtectionManager { static let shared = DataBrokerProtectionManager() private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() - private let authenticationRepository: AuthenticationRepository = KeychainAuthenticationData() - private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() - private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() private let dataBrokerProtectionWaitlistDataSource: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .dbp) @@ -56,13 +54,11 @@ public final class DataBrokerProtectionManager { }() private init() { - self.redeemUseCase = RedeemUseCase(authenticationService: authenticationService, - authenticationRepository: authenticationRepository) - + self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager() } - public func shouldAskForInviteCode() -> Bool { - redeemUseCase.shouldAskForInviteCode() + public func isUserAuthenticated() -> Bool { + authenticationManager.isUserAuthenticated } // MARK: - Debugging Features diff --git a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift index 57dd6d5c33..50f8a36d4b 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift @@ -23,34 +23,16 @@ import DataBrokerProtection import PixelKit final class DataBrokerProtectionSubscriptionEventHandler { - - private let accountManager: AccountManaging - private let authRepository: AuthenticationRepository private let featureDisabler: DataBrokerProtectionFeatureDisabling - init(accountManager: AccountManaging = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)), - authRepository: AuthenticationRepository = KeychainAuthenticationData(), - featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { - self.accountManager = accountManager - self.authRepository = authRepository + init(featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { self.featureDisabler = featureDisabler } func registerForSubscriptionAccountManagerEvents() { - NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignIn), name: .accountDidSignIn, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil) } - @objc private func handleAccountDidSignIn() { - guard let token = accountManager.accessToken else { - PixelKit.fire(GeneralPixel.dataBrokerProtectionErrorWhenFetchingSubscriptionAuthTokenAfterSignIn) - assertionFailure("[DBP Subscription] AccountManager signed in but token could not be retrieved") - return - } - - authRepository.save(accessToken: token) - } - @objc private func handleAccountDidSignOut() { featureDisabler.disableAndDelete() } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 90cb21ae2b..0380cedd98 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -140,9 +140,6 @@ enum GeneralPixel: PixelKitEventV2 { case dataBrokerResetLoginItemDaily case dataBrokerDisableAndDeleteDaily - // DataBrokerProtection Other - case dataBrokerProtectionErrorWhenFetchingSubscriptionAuthTokenAfterSignIn - // Default Browser case defaultRequestedFromHomepage case defaultRequestedFromHomepageSetupView @@ -507,8 +504,6 @@ enum GeneralPixel: PixelKitEventV2 { return "m_mac_dbp_imp_terms" case .dataBrokerProtectionWaitlistTermsAndConditionsAccepted: return "m_mac_dbp_ev_terms_accepted" - case .dataBrokerProtectionErrorWhenFetchingSubscriptionAuthTokenAfterSignIn: - return "m_mac_dbp_error_when_fetching_subscription_auth_token_after_sign_in" case .dataBrokerProtectionRemoteMessageDisplayed(let messageID): return "m_mac_dbp_remote_message_displayed_\(messageID)" case .dataBrokerProtectionRemoteMessageDismissed(let messageID): diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift new file mode 100644 index 0000000000..594eb0b3d3 --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift @@ -0,0 +1,38 @@ +// +// DataBrokerAuthenticationManagerBuilder.swift +// +// Copyright © 2024 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 Foundation +import DataBrokerProtection +import Subscription + +final public class DataBrokerAuthenticationManagerBuilder { + static func buildAuthenticationManager(redeemUseCase: RedeemUseCase = RedeemUseCase()) -> DataBrokerProtectionAuthenticationManager { + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let subscriptionManager = DataBrokerProtectionSubscriptionManager(accountManager: accountManager, + environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManager()) + return DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + } +} + +extension AccountManager: DataBrokerProtectionAccountManaging { + public func hasEntitlement(for cachePolicy: CachePolicy) async -> Result { + await hasEntitlement(for: .dataBrokerProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift index 2bdc06947a..459f52fce6 100644 --- a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift @@ -21,6 +21,7 @@ import Common import BrowserServicesKit import DataBrokerProtection import PixelKit +import Subscription public final class DataBrokerProtectionBackgroundManager { @@ -30,7 +31,7 @@ public final class DataBrokerProtectionBackgroundManager { private let authenticationRepository: AuthenticationRepository = KeychainAuthenticationData() private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() - private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() private lazy var ipcServiceManager = IPCServiceManager(scheduler: scheduler, pixelHandler: pixelHandler) @@ -65,13 +66,15 @@ public final class DataBrokerProtectionBackgroundManager { dataManager: dataManager, notificationCenter: NotificationCenter.default, pixelHandler: pixelHandler, - redeemUseCase: redeemUseCase, + authenticationManager: authenticationManager, userNotificationService: userNotificationService) }() private init() { - self.redeemUseCase = RedeemUseCase(authenticationService: authenticationService, - authenticationRepository: authenticationRepository) + let redeemUseCase = RedeemUseCase(authenticationService: authenticationService, + authenticationRepository: authenticationRepository) + self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase) + _ = ipcServiceManager } diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgent.entitlements b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgent.entitlements index c1bf3bf0e0..fc132e719b 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgent.entitlements +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgent.entitlements @@ -11,6 +11,7 @@ keychain-access-groups $(DBP_APP_GROUP) + $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppStore.entitlements b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppStore.entitlements index b90c211a88..19c29d3d2f 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppStore.entitlements +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppStore.entitlements @@ -15,6 +15,7 @@ keychain-access-groups $(DBP_APP_GROUP) + $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGoDBPBackgroundAgent/Info-AppStore.plist b/DuckDuckGoDBPBackgroundAgent/Info-AppStore.plist index cbd58651f6..86eede1b79 100644 --- a/DuckDuckGoDBPBackgroundAgent/Info-AppStore.plist +++ b/DuckDuckGoDBPBackgroundAgent/Info-AppStore.plist @@ -2,14 +2,14 @@ - DBP_APP_GROUP - $(DBP_APP_GROUP) - LSApplicationCategoryType - public.app-category.productivity - NSAppTransportSecurity - - NSAllowsArbitraryLoadsInWebContent - - + DBP_APP_GROUP + $(DBP_APP_GROUP) + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + + SUBSCRIPTION_APP_GROUP + $(SUBSCRIPTION_APP_GROUP) diff --git a/DuckDuckGoDBPBackgroundAgent/Info.plist b/DuckDuckGoDBPBackgroundAgent/Info.plist index cbd58651f6..86eede1b79 100644 --- a/DuckDuckGoDBPBackgroundAgent/Info.plist +++ b/DuckDuckGoDBPBackgroundAgent/Info.plist @@ -2,14 +2,14 @@ - DBP_APP_GROUP - $(DBP_APP_GROUP) - LSApplicationCategoryType - public.app-category.productivity - NSAppTransportSecurity - - NSAllowsArbitraryLoadsInWebContent - - + DBP_APP_GROUP + $(DBP_APP_GROUP) + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + + SUBSCRIPTION_APP_GROUP + $(SUBSCRIPTION_APP_GROUP) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift new file mode 100644 index 0000000000..0aea2ac3ef --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionAuthenticationManaging.swift @@ -0,0 +1,69 @@ +// +// DataBrokerProtectionAuthenticationManaging.swift +// +// Copyright © 2024 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 Foundation + +public protocol DataBrokerProtectionAuthenticationManaging { + var isUserAuthenticated: Bool { get } + var accessToken: String? { get } + func hasValidEntitlement() async throws -> Bool + func shouldAskForInviteCode() -> Bool + func redeem(inviteCode: String) async throws + func getAuthHeader() -> String? +} + +public final class DataBrokerProtectionAuthenticationManager: DataBrokerProtectionAuthenticationManaging { + private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let subscriptionManager: DataBrokerProtectionSubscriptionManaging + + public var isUserAuthenticated: Bool { + subscriptionManager.isUserAuthenticated + } + + public var accessToken: String? { + subscriptionManager.accessToken + } + + public init(redeemUseCase: any DataBrokerProtectionRedeemUseCase, + subscriptionManager: any DataBrokerProtectionSubscriptionManaging) { + self.redeemUseCase = redeemUseCase + self.subscriptionManager = subscriptionManager + } + + public func hasValidEntitlement() async throws -> Bool { + try await subscriptionManager.hasValidEntitlement() + } + + public func getAuthHeader() -> String? { + ServicesAuthHeaderBuilder().getAuthHeader(accessToken) + } + + // MARK: - Redeem code flow + + // We might want the ability to ask for invite code later on, keeping this here to make things easier + // https://app.asana.com/0/1204167627774280/1207270521849479/f + + public func shouldAskForInviteCode() -> Bool { + // redeemUseCase.shouldAskForInviteCode() + return false + } + + public func redeem(inviteCode: String) async throws { + // await redeemUseCase.redeem(inviteCode: inviteCode) + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift new file mode 100644 index 0000000000..4b94e75351 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift @@ -0,0 +1,65 @@ +// +// DataBrokerProtectionSubscriptionManaging.swift +// +// Copyright © 2024 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 Foundation +import Subscription +import Common + +public protocol DataBrokerProtectionSubscriptionManaging { + var isUserAuthenticated: Bool { get } + var accessToken: String? { get } + func hasValidEntitlement() async throws -> Bool +} + +public final class DataBrokerProtectionSubscriptionManager: DataBrokerProtectionSubscriptionManaging { + private let accountManager: DataBrokerProtectionAccountManaging + private let environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging + + public var isUserAuthenticated: Bool { + accountManager.accessToken != nil + } + + public var accessToken: String? { + accountManager.accessToken + } + + public init(accountManager: DataBrokerProtectionAccountManaging, + environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging) { + self.accountManager = accountManager + self.environmentManager = environmentManager + } + + public func hasValidEntitlement() async throws -> Bool { + environmentManager.updateEnvironment() + + switch await accountManager.hasEntitlement(for: .reloadIgnoringLocalCacheData) { + case let .success(result): + return result + case .failure(let error): + throw error + } + } +} + +// MARK: - Wrapper Protocols + +/// This protocol exists only as a wrapper on top of the AccountManager since it is a concrete type on BSK +public protocol DataBrokerProtectionAccountManaging { + var accessToken: String? { get } + func hasEntitlement(for cachePolicy: AccountManager.CachePolicy) async -> Result +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift new file mode 100644 index 0000000000..a18796bace --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift @@ -0,0 +1,37 @@ +// +// DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift +// +// Copyright © 2024 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 Foundation +import Subscription + +/// This protocol exists only as a wrapper on top of the SubscriptionPurchaseEnvironment since it is a concrete type on BSK +public protocol DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging { + func updateEnvironment() +} + +public final class DataBrokerProtectionSubscriptionPurchaseEnvironmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging { + private let settings: DataBrokerProtectionSettings + + public init(settings: DataBrokerProtectionSettings = DataBrokerProtectionSettings()) { + self.settings = settings + } + + public func updateEnvironment() { + SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift index 8ef73fc3f4..46deebb2c3 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewController.swift @@ -20,8 +20,19 @@ import Foundation import SwiftUI public final class DataBrokerRunCustomJSONViewController: NSViewController { + private let authenticationManager: DataBrokerProtectionAuthenticationManaging + + public init(authenticationManager: DataBrokerProtectionAuthenticationManaging) { + self.authenticationManager = authenticationManager + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + public override func loadView() { - let contentView = DataBrokerRunCustomJSONView(viewModel: DataBrokerRunCustomJSONViewModel()) + let contentView = DataBrokerRunCustomJSONView(viewModel: DataBrokerRunCustomJSONViewModel(authenticationManager: authenticationManager)) let hostingController = NSHostingController(rootView: contentView) hostingController.view.autoresizingMask = [.width, .height] self.view = hostingController.view diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index 3cf0289b99..75c2f1ab78 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -150,8 +150,9 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { } private let contentScopeProperties: ContentScopeProperties private let csvColumns = ["name_input", "age_input", "city_input", "state_input", "name_scraped", "age_scraped", "address_scraped", "relatives_scraped", "url", "broker name", "screenshot_id", "error", "matched_fields", "result_match", "expected_match"] + private let authenticationManager: DataBrokerProtectionAuthenticationManaging - init() { + init(authenticationManager: DataBrokerProtectionAuthenticationManaging) { let privacyConfigurationManager = PrivacyConfigurationManagingMock() let features = ContentScopeFeatureToggles(emailProtection: false, emailProtectionIncontextSignup: false, @@ -164,6 +165,7 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { thirdPartyCredentialsProvider: false) let sessionKey = UUID().uuidString + self.authenticationManager = authenticationManager let contentScopeProperties = ContentScopeProperties(gpcEnabled: false, sessionKey: sessionKey, featureToggles: features) @@ -171,8 +173,8 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { self.runnerProvider = DataBrokerOperationRunnerProvider( privacyConfigManager: privacyConfigurationManager, contentScopeProperties: contentScopeProperties, - emailService: EmailService(), - captchaService: CaptchaService()) + emailService: EmailService(authenticationManager: authenticationManager), + captchaService: CaptchaService(authenticationManager: authenticationManager)) self.privacyConfigManager = privacyConfigurationManager self.contentScopeProperties = contentScopeProperties @@ -190,7 +192,11 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { try await withThrowingTaskGroup(of: DebugScanReturnValue.self) { group in for queryData in brokerProfileQueryData { - let debugScanOperation = DebugScanOperation(privacyConfig: self.privacyConfigManager, prefs: self.contentScopeProperties, query: queryData) { + let debugScanOperation = DebugScanOperation(privacyConfig: self.privacyConfigManager, + prefs: self.contentScopeProperties, + query: queryData, + emailService: EmailService(authenticationManager: self.authenticationManager), + captchaService: CaptchaService(authenticationManager: self.authenticationManager)) { true } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift index bc6ff398d2..364be47c18 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanOperation.swift @@ -78,8 +78,8 @@ final class DebugScanOperation: DataBrokerOperation { init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, query: BrokerProfileQueryData, - emailService: EmailServiceProtocol = EmailService(), - captchaService: CaptchaServiceProtocol = CaptchaService(), + emailService: EmailServiceProtocol, + captchaService: CaptchaServiceProtocol, operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 0, shouldRunNextStep: @escaping () -> Bool diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift index 9f48f0faa1..98b29f6827 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutOperation.swift @@ -54,8 +54,8 @@ final class OptOutOperation: DataBrokerOperation { init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, query: BrokerProfileQueryData, - emailService: EmailServiceProtocol = EmailService(), - captchaService: CaptchaServiceProtocol = CaptchaService(), + emailService: EmailServiceProtocol, + captchaService: CaptchaServiceProtocol, cookieHandler: CookieHandler = BrokerCookieHandler(), operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 40, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift index 964c7cdf69..7f7ba274a9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanOperation.swift @@ -48,8 +48,8 @@ final class ScanOperation: DataBrokerOperation { init(privacyConfig: PrivacyConfigurationManaging, prefs: ContentScopeProperties, query: BrokerProfileQueryData, - emailService: EmailServiceProtocol = EmailService(), - captchaService: CaptchaServiceProtocol = CaptchaService(), + emailService: EmailServiceProtocol, + captchaService: CaptchaServiceProtocol, cookieHandler: CookieHandler = BrokerCookieHandler(), operationAwaitTime: TimeInterval = 3, clickAwaitTime: TimeInterval = 0, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index 509b13272c..b0f0d324b6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -131,6 +131,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch private let captchaService: CaptchaServiceProtocol private let userNotificationService: DataBrokerProtectionUserNotificationService private var currentOperation: DataBrokerProtectionCurrentOperation = .idle + private let authenticationManager: DataBrokerProtectionAuthenticationManaging /// Ensures that only one scheduler operation is executed at the same time. /// @@ -161,7 +162,7 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch dataManager: DataBrokerProtectionDataManager, notificationCenter: NotificationCenter = NotificationCenter.default, pixelHandler: EventMapping, - redeemUseCase: DataBrokerProtectionRedeemUseCase, + authenticationManager: DataBrokerProtectionAuthenticationManaging, userNotificationService: DataBrokerProtectionUserNotificationService ) { activity = NSBackgroundActivityScheduler(identifier: schedulerIdentifier) @@ -176,9 +177,10 @@ public final class DefaultDataBrokerProtectionScheduler: DataBrokerProtectionSch self.pixelHandler = pixelHandler self.notificationCenter = notificationCenter self.userNotificationService = userNotificationService + self.authenticationManager = authenticationManager - self.emailService = EmailService(redeemUseCase: redeemUseCase) - self.captchaService = CaptchaService(redeemUseCase: redeemUseCase) + self.emailService = EmailService(authenticationManager: authenticationManager) + self.captchaService = CaptchaService(authenticationManager: authenticationManager) } public func startScheduler(showWebView: Bool = false) { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift index c249359ef8..b29112dab0 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift @@ -123,16 +123,16 @@ struct CaptchaService: CaptchaServiceProtocol { } private let urlSession: URLSession - private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let settings: DataBrokerProtectionSettings private let servicePixel: DataBrokerProtectionBackendServicePixels init(urlSession: URLSession = URLSession.shared, - redeemUseCase: DataBrokerProtectionRedeemUseCase = RedeemUseCase(), + authenticationManager: DataBrokerProtectionAuthenticationManaging, settings: DataBrokerProtectionSettings = DataBrokerProtectionSettings(), servicePixel: DataBrokerProtectionBackendServicePixels = DefaultDataBrokerProtectionBackendServicePixels()) { self.urlSession = urlSession - self.redeemUseCase = redeemUseCase + self.authenticationManager = authenticationManager self.settings = settings self.servicePixel = servicePixel } @@ -186,7 +186,7 @@ struct CaptchaService: CaptchaServiceProtocol { os_log("Submitting captcha request ...", log: .service) var request = URLRequest(url: url) - guard let authHeader = redeemUseCase.getAuthHeader() else { + guard let authHeader = authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .submitCaptchaInformationRequest) throw AuthenticationError.noAuthToken } @@ -272,7 +272,7 @@ struct CaptchaService: CaptchaServiceProtocol { } var request = URLRequest(url: url) - guard let authHeader = redeemUseCase.getAuthHeader() else { + guard let authHeader = authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .submitCaptchaToBeResolvedRequest) throw AuthenticationError.noAuthToken } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift index 8cd57ea70c..48cd0364e7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift @@ -51,16 +51,16 @@ struct EmailService: EmailServiceProtocol { } public let urlSession: URLSession - private let redeemUseCase: DataBrokerProtectionRedeemUseCase + private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let settings: DataBrokerProtectionSettings private let servicePixel: DataBrokerProtectionBackendServicePixels init(urlSession: URLSession = URLSession.shared, - redeemUseCase: DataBrokerProtectionRedeemUseCase = RedeemUseCase(), + authenticationManager: DataBrokerProtectionAuthenticationManaging, settings: DataBrokerProtectionSettings = DataBrokerProtectionSettings(), servicePixel: DataBrokerProtectionBackendServicePixels = DefaultDataBrokerProtectionBackendServicePixels()) { self.urlSession = urlSession - self.redeemUseCase = redeemUseCase + self.authenticationManager = authenticationManager self.settings = settings self.servicePixel = servicePixel } @@ -78,7 +78,7 @@ struct EmailService: EmailServiceProtocol { } var request = URLRequest(url: url) - guard let authHeader = redeemUseCase.getAuthHeader() else { + guard let authHeader = authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .getEmail) throw AuthenticationError.noAuthToken } @@ -161,7 +161,7 @@ struct EmailService: EmailServiceProtocol { var request = URLRequest(url: url) - guard let authHeader = redeemUseCase.getAuthHeader() else { + guard let authHeader = authenticationManager.getAuthHeader() else { servicePixel.fireEmptyAccessToken(callSite: .extractEmailLink) throw AuthenticationError.noAuthToken } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/RedeemCodeServices.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/RedeemCodeServices.swift index 60a71ad658..7c446cac03 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/RedeemCodeServices.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/RedeemCodeServices.swift @@ -77,10 +77,7 @@ public final class RedeemUseCase: DataBrokerProtectionRedeemUseCase { } public func getAuthHeader() -> String? { - guard let token = authenticationRepository.getAccessToken() else { - return nil - } - return "bearer \(token)" + ServicesAuthHeaderBuilder().getAuthHeader(authenticationRepository.getAccessToken()) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/ServicesAuthHeaderBuilder.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/ServicesAuthHeaderBuilder.swift new file mode 100644 index 0000000000..59d6316877 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/ServicesAuthHeaderBuilder.swift @@ -0,0 +1,38 @@ +// +// ServicesAuthHeaderBuilder.swift +// +// Copyright © 2024 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 Foundation + +struct ServicesAuthHeaderBuilder { + + /** + * Receives an auth token and returns the header value as expected by our services + * + * - Parameters: + * - token: The authentication token to be included in the header + * + * - Returns: The formatted header value with the token included, or nil if the token is nil or empty + */ + public func getAuthHeader(_ token: String?) -> String? { + guard let token = token, !token.isEmpty else { + return nil + } + return "bearer \(token)" + } + +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/CaptchaServiceTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/CaptchaServiceTests.swift index 9eca0082dd..16596cd6f1 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/CaptchaServiceTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/CaptchaServiceTests.swift @@ -23,6 +23,7 @@ import Foundation final class CaptchaServiceTests: XCTestCase { private let servicePixel = MockDataBrokerProtectionBackendServicePixels() let jsonEncoder = JSONEncoder() + private let mockAuthenticationManager = MockAuthenticationManager() enum MockError: Error { case someError @@ -37,12 +38,13 @@ final class CaptchaServiceTests: XCTestCase { override func tearDown() async throws { MockURLProtocol.requestHandlerQueue.removeAll() servicePixel.reset() + mockAuthenticationManager.reset() } func testWhenSessionThrowsOnSubmittingCaptchaInfo_thenTheCorrectErrorIsThrown() async { MockURLProtocol.requestHandlerQueue.append({ _ in throw MockError.someError }) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -62,7 +64,7 @@ final class CaptchaServiceTests: XCTestCase { let response = CaptchaTransaction(message: .failureCritical, transactionId: nil) MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, try? self.jsonEncoder.encode(response)) }) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -82,7 +84,7 @@ final class CaptchaServiceTests: XCTestCase { let response = CaptchaTransaction(message: .invalidRequest, transactionId: nil) MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, try? self.jsonEncoder.encode(response)) }) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -106,7 +108,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -128,7 +130,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -150,7 +152,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -172,7 +174,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -196,7 +198,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -218,7 +220,7 @@ final class CaptchaServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(requestHandler) let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -231,11 +233,10 @@ final class CaptchaServiceTests: XCTestCase { } func testWhenNoAuthTokenAvailable_noAuthTokenErrorIsThrown() async { - let redeemUseCase = MockRedeemUseCase() - redeemUseCase.shouldSendNilAuthHeader = true + mockAuthenticationManager.authHeaderValue = nil let sut = CaptchaService(urlSession: mockURLSession, - redeemUseCase: redeemUseCase, + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift new file mode 100644 index 0000000000..b0603c8399 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAuthenticationManagerTests.swift @@ -0,0 +1,141 @@ +// +// DataBrokerProtectionAuthenticationManagerTests.swift +// +// Copyright © 2023 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 DataBrokerProtection + +class DataBrokerProtectionAuthenticationManagerTests: XCTestCase { + var authenticationManager: DataBrokerProtectionAuthenticationManager! + var redeemUseCase: DataBrokerProtectionRedeemUseCase! + var subscriptionManager: MockDataBrokerProtectionSubscriptionManaging! + + override func setUp() async throws { + redeemUseCase = MockRedeemUseCase() + subscriptionManager = MockDataBrokerProtectionSubscriptionManaging() + } + + override func tearDown() async throws { + authenticationManager = nil + redeemUseCase = nil + subscriptionManager = nil + } + + func testUserNotAuthenticatedWhenSubscriptionManagerReturnsFalse() { + subscriptionManager.userAuthenticatedValue = false + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + XCTAssertEqual(authenticationManager.isUserAuthenticated, false) + } + + func testEmptyAccessTokenResultsInNilAuthHeader() { + subscriptionManager.accessTokenValue = nil + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + XCTAssertNil(authenticationManager.getAuthHeader()) + } + + func testUserAuthenticatedWhenSubscriptionManagerReturnsTrue() { + subscriptionManager.userAuthenticatedValue = true + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + XCTAssertEqual(authenticationManager.isUserAuthenticated, true) + } + + func testNonEmptyAccessTokenResultsInValidAuthHeader() { + let accessToken = "validAccessToken" + subscriptionManager.accessTokenValue = accessToken + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + XCTAssertNotNil(authenticationManager.getAuthHeader()) + } + + func testValidEntitlementCheckWithSuccess() async { + subscriptionManager.entitlementResultValue = true + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + do { + let result = try await authenticationManager.hasValidEntitlement() + XCTAssertTrue(result, "Entitlement check should return true for valid entitlement") + } catch { + XCTFail("Entitlement check should not fail: \(error)") + } + } + + func testValidEntitlementCheckWithSuccessFalse() async { + subscriptionManager.entitlementResultValue = false + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + do { + let result = try await authenticationManager.hasValidEntitlement() + XCTAssertFalse(result, "Entitlement check should return false for valid entitlement") + } catch { + XCTFail("Entitlement check should not fail: \(error)") + } + } + + func testValidEntitlementCheckWithFailure() async { + let mockError = NSError(domain: "TestErrorDomain", code: 123, userInfo: nil) + subscriptionManager.entitlementError = mockError + + authenticationManager = DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + do { + _ = try await authenticationManager.hasValidEntitlement() + XCTFail("Entitlement check should fail") + } catch let error as NSError { + XCTAssertEqual(mockError.domain, error.domain) + XCTAssertEqual(mockError.code, error.code) + } + } +} + +final class MockDataBrokerProtectionSubscriptionManaging: DataBrokerProtectionSubscriptionManaging { + typealias EntitlementResult = Result + + var userAuthenticatedValue = false + var accessTokenValue: String? + var entitlementResultValue = false + var entitlementError: Error? + + var isUserAuthenticated: Bool { + userAuthenticatedValue + } + + var accessToken: String? { + accessTokenValue + } + + func hasValidEntitlement() async throws -> Bool { + if let error = entitlementError { + throw error + } + return entitlementResultValue + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift index 66bf08995e..dc853279ae 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/EmailServiceTests.swift @@ -22,6 +22,7 @@ import Foundation final class EmailServiceTests: XCTestCase { private let servicePixel = MockDataBrokerProtectionBackendServicePixels() + private let mockAuthenticationManager = MockAuthenticationManager() enum MockError: Error { case someError @@ -36,12 +37,13 @@ final class EmailServiceTests: XCTestCase { override func tearDown() async throws { MockURLProtocol.requestHandlerQueue.removeAll() servicePixel.reset() + mockAuthenticationManager.reset() } func testWhenSessionThrows_thenTheCorrectErrorIsThrown() async { MockURLProtocol.requestHandlerQueue.append({ _ in throw MockError.someError }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -65,7 +67,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -87,7 +89,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -106,7 +108,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(unknownResponse) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -136,7 +138,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(notReadyResponse) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -169,7 +171,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append(successResponse) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -193,7 +195,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -220,7 +222,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -247,7 +249,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.ok, responseData) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -266,10 +268,9 @@ final class EmailServiceTests: XCTestCase { } func testWhenNoAuthTokenAvailable_noAuthTokenErrorIsThrown() async { - let redeemUseCase = MockRedeemUseCase() - redeemUseCase.shouldSendNilAuthHeader = true + mockAuthenticationManager.authHeaderValue = nil let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: redeemUseCase, + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) @@ -290,7 +291,7 @@ final class EmailServiceTests: XCTestCase { MockURLProtocol.requestHandlerQueue.append({ _ in (HTTPURLResponse.noAuth, nil) }) let sut = EmailService(urlSession: mockURLSession, - redeemUseCase: MockRedeemUseCase(), + authenticationManager: mockAuthenticationManager, settings: DataBrokerProtectionSettings(defaults: .standard), servicePixel: servicePixel) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 9b60fe4812..99f01aa8ff 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -960,3 +960,35 @@ final class MockDataBrokerProtectionBackendServicePixels: DataBrokerProtectionBa statusCode = nil } } + +final class MockAuthenticationManager: DataBrokerProtectionAuthenticationManaging { + var isUserAuthenticatedValue = false + var accessTokenValue: String? = "fake token" + var shouldAskForInviteCodeValue = false + var redeemCodeCalled = false + var authHeaderValue: String? = "fake auth header" + + var isUserAuthenticated: Bool { isUserAuthenticatedValue } + + var accessToken: String? { accessTokenValue } + + func hasValidEntitlement() async throws -> Bool { + return true + } + + func shouldAskForInviteCode() -> Bool { shouldAskForInviteCodeValue } + + func redeem(inviteCode: String) async throws { + redeemCodeCalled = true + } + + func getAuthHeader() -> String? { authHeaderValue } + + func reset() { + isUserAuthenticatedValue = false + accessTokenValue = "fake token" + shouldAskForInviteCodeValue = false + redeemCodeCalled = false + authHeaderValue = "fake auth header" + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ServicesAuthHeaderBuilderTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ServicesAuthHeaderBuilderTests.swift new file mode 100644 index 0000000000..ac3844e0dd --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/ServicesAuthHeaderBuilderTests.swift @@ -0,0 +1,72 @@ +// +// ServicesAuthHeaderBuilderTests.swift +// +// Copyright © 2023 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 DataBrokerProtection + +final class ServicesAuthHeaderBuilderTests: XCTestCase { + + func testValidToken() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("validToken123") + XCTAssertEqual(result, "bearer validToken123") + } + + func testNilToken() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader(nil) + XCTAssertNil(result) + } + + func testEmptyToken() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("") + XCTAssertNil(result) + } + + func testTokenWithSpaces() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader(" tokenWithSpaces ") + XCTAssertEqual(result, "bearer tokenWithSpaces ") + } + + func testTokenWithSpecialCharacters() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("token@123!#") + XCTAssertEqual(result, "bearer token@123!#") + } + + func testLongToken() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("averylongtokenthatneedstobeincludedintheheader") + XCTAssertEqual(result, "bearer averylongtokenthatneedstobeincludedintheheader") + } + + func testTokenWithLeadingBearer() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("bearer token123") + XCTAssertEqual(result, "bearer bearer token123") + } + + func testTokenWithLeadingBearerUppercase() { + let builder = ServicesAuthHeaderBuilder() + let result = builder.getAuthHeader("Bearer token456") + XCTAssertEqual(result, "bearer Bearer token456") + } + +}