diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a6b80e830d..2f919b5f0b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2747,6 +2747,8 @@ BDCB66D92C7CE1A700E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */; }; BDE981D92BBD10D600645880 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BDE981DA2BBD10D600645880 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + C126B35A2C820924005DC2A3 /* FreemiumDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */; }; + C126B35B2C820924005DC2A3 /* FreemiumDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */; }; C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; }; @@ -4552,6 +4554,7 @@ BDBA859B2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModel.swift; sourceTree = ""; }; BDBA859E2C5D25B700BC54F5 /* VPNMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNMetadataCollector.swift; sourceTree = ""; }; BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = ""; }; + C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreemiumDebugMenu.swift; sourceTree = ""; }; C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; @@ -9028,6 +9031,7 @@ isa = PBXGroup; children = ( C1858CD02C7C95E500C9BEAB /* PIR */, + C126B3592C820924005DC2A3 /* FreemiumDebugMenu.swift */, ); path = Freemium; sourceTree = ""; @@ -10791,6 +10795,7 @@ 3706FB2C293F65D500E42796 /* UserDefaultsWrapper.swift in Sources */, 3706FB2D293F65D500E42796 /* PasswordManagementPopover.swift in Sources */, 3706FB2F293F65D500E42796 /* HomePageRecentlyVisitedModel.swift in Sources */, + C126B35B2C820924005DC2A3 /* FreemiumDebugMenu.swift in Sources */, 3707C718294B5D0F00682A9F /* AdClickAttributionTabExtension.swift in Sources */, 31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */, 3706FEBA293F6EFF00E42796 /* BWStatus.swift in Sources */, @@ -12879,6 +12884,7 @@ 85589E8327BBB8630038AD11 /* HomePageViewController.swift in Sources */, F17114852C7C9D28009836C1 /* Logger+Fire.swift in Sources */, 5614B3A12BBD639D009B5031 /* ZoomPopover.swift in Sources */, + C126B35A2C820924005DC2A3 /* FreemiumDebugMenu.swift in Sources */, B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */, 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */, 1D36F4242A3B85C50052B527 /* TabCleanupPreparer.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 6431b1ca29..7132573f5e 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -40,6 +40,7 @@ import NetworkProtectionIPC import DataBrokerProtection import RemoteMessaging import os.log +import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { @@ -383,7 +384,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { UNUserNotificationCenter.current().delegate = self dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() - DataBrokerProtectionAppEvents(featureGatekeeper: DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: subscriptionManager.accountManager)).applicationDidFinishLaunching() + + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: + subscriptionManager.accountManager, + freemiumPIRUserStateManager: freemiumPIRUserStateManager) + + DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidFinishLaunching() setUpAutoClearHandler() @@ -430,9 +437,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate { NetworkProtectionAppEvents(featureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() - DataBrokerProtectionAppEvents(featureGatekeeper: - DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: - subscriptionManager.accountManager)).applicationDidBecomeActive() + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let pirGatekeeper = DefaultDataBrokerProtectionFeatureGatekeeper(accountManager: + subscriptionManager.accountManager, + freemiumPIRUserStateManager: freemiumPIRUserStateManager) + + DataBrokerProtectionAppEvents(featureGatekeeper: pirGatekeeper).applicationDidBecomeActive() subscriptionManager.refreshCachedSubscriptionAndEntitlements { isSubscriptionActive in if isSubscriptionActive { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift index 0affd1fb34..d0705f32b5 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift @@ -22,11 +22,10 @@ import Common import DataBrokerProtection import Subscription import os.log +import Freemium protocol DataBrokerProtectionFeatureGatekeeper { - func isFeatureVisible() -> Bool func disableAndDeleteForAllUsers() - func isPrivacyProEnabled() -> Bool func arePrerequisitesSatisfied() async -> Bool } @@ -37,19 +36,22 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature private let userDefaults: UserDefaults private let subscriptionAvailability: SubscriptionFeatureAvailability private let accountManager: AccountManager + private let freemiumPIRUserStateManager: FreemiumPIRUserStateManager init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler(), pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler(), userDefaults: UserDefaults = .standard, subscriptionAvailability: SubscriptionFeatureAvailability = DefaultSubscriptionFeatureAvailability(), - accountManager: AccountManager) { + accountManager: AccountManager, + freemiumPIRUserStateManager: FreemiumPIRUserStateManager) { self.privacyConfigurationManager = privacyConfigurationManager self.featureDisabler = featureDisabler self.pixelHandler = pixelHandler self.userDefaults = userDefaults self.subscriptionAvailability = subscriptionAvailability self.accountManager = accountManager + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager } var isUserLocaleAllowed: Bool { @@ -70,28 +72,23 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature return (regionCode ?? "US") == "US" } - func isPrivacyProEnabled() -> Bool { - return subscriptionAvailability.isFeatureAvailable - } - func disableAndDeleteForAllUsers() { featureDisabler.disableAndDelete() Logger.dataBrokerProtection.debug("Disabling and removing DBP for all users") } - /// If we want to prevent new users from joining the waitlist while still allowing waitlist users to continue using it, - /// we should set isWaitlistEnabled to false and isWaitlistBetaActive to true. - /// To remove it from everyone, isWaitlistBetaActive should be set to false - func isFeatureVisible() -> Bool { - // only US locale should be available - guard isUserLocaleAllowed else { return false } + /// Checks PIR prerequisites + /// + /// Prerequisites are satisified if either: + /// 1. The user is an active freemium user + /// 2. The user has a subscription with valid entitlements + /// + /// - Returns: Bool indicating prerequisites are satisfied + func arePrerequisitesSatisfied() async -> Bool { - // US internal users should have it available by default - return isInternalUser - } + if freemiumPIRUserStateManager.isActiveUser { return true } - func arePrerequisitesSatisfied() async -> Bool { let entitlements = await accountManager.hasEntitlement(forProductName: .dataBrokerProtection, cachePolicy: .reloadIgnoringLocalCacheData) var hasEntitlements: Bool diff --git a/DuckDuckGo/Freemium/FreemiumDebugMenu.swift b/DuckDuckGo/Freemium/FreemiumDebugMenu.swift new file mode 100644 index 0000000000..7e46510a4f --- /dev/null +++ b/DuckDuckGo/Freemium/FreemiumDebugMenu.swift @@ -0,0 +1,52 @@ +// +// FreemiumDebugMenu.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 Freemium + +final class FreemiumDebugMenu: NSMenuItem { + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public init() { + super.init(title: "Freemium", action: nil, keyEquivalent: "") + self.submenu = makeSubmenu() + } + + private func makeSubmenu() -> NSMenu { + let menu = NSMenu(title: "") + + menu.addItem(NSMenuItem(title: "Set Freemium PIR Onboarded State TRUE", action: #selector(setFreemiumPIROnboardStateEnabled), target: self)) + menu.addItem(NSMenuItem(title: "Set Freemium PIR Onboarded State FALSE", action: #selector(setFreemiumPIROnboardStateDisabled), target: self)) + menu.addItem(.separator()) + + return menu + } + + @objc + func setFreemiumPIROnboardStateEnabled() { + DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: Application.appDelegate.subscriptionManager.accountManager).didOnboard = true + } + + @objc + func setFreemiumPIROnboardStateDisabled() { + DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: Application.appDelegate.subscriptionManager.accountManager).didOnboard = false + } +} diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 5b8a83aa6b..e7506fa236 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -33,7 +33,7 @@ final class HomePageViewController: NSViewController { private let fireViewModel: FireViewModel private let onboardingViewModel: OnboardingViewModel private let freemiumPIRFeature: FreemiumPIRFeature - private var freemiumPIRUserState: FreemiumPIRUserState + private var freemiumPIRUserStateManager: FreemiumPIRUserStateManager private let freemiumPIRPresenter: FreemiumPIRPresenter private(set) lazy var faviconsFetcherOnboarding: FaviconsFetcherOnboarding? = { @@ -71,7 +71,7 @@ final class HomePageViewController: NSViewController { appearancePreferences: AppearancePreferences = AppearancePreferences.shared, defaultBrowserPreferences: DefaultBrowserPreferences = DefaultBrowserPreferences.shared, freemiumPIRFeature: FreemiumPIRFeature, - freemiumPIRUserState: FreemiumPIRUserState, + freemiumPIRUserStateManager: FreemiumPIRUserStateManager, freemiumPIRPresenter: FreemiumPIRPresenter = DefaultFreemiumPIRPresenter()) { self.tabCollectionViewModel = tabCollectionViewModel @@ -83,7 +83,7 @@ final class HomePageViewController: NSViewController { self.appearancePreferences = appearancePreferences self.defaultBrowserPreferences = defaultBrowserPreferences self.freemiumPIRFeature = freemiumPIRFeature - self.freemiumPIRUserState = freemiumPIRUserState + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager self.freemiumPIRPresenter = freemiumPIRPresenter super.init(nibName: nil, bundle: nil) @@ -221,9 +221,9 @@ final class HomePageViewController: NSViewController { private func createPromotionModel() -> PromotionViewModel { return PromotionViewModel.freemiumPIRPromotion { [weak self] in // TODO: Remove this - self?.freemiumPIRUserState.didOnboard = true + self?.freemiumPIRUserStateManager.didOnboard = true // ------ - self?.freemiumPIRPresenter.showFreemiumPIR(didOnboard: self?.freemiumPIRUserState.didOnboard ?? false, + self?.freemiumPIRPresenter.showFreemiumPIR(didOnboard: self?.freemiumPIRUserStateManager.didOnboard ?? false, windowControllerManager: WindowControllersManager.shared) self?.appearancePreferences.didDismissHomePagePromotion = true } closeAction: { [weak self] in diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 438a4a8e1b..a848c523c5 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -635,6 +635,8 @@ final class MainMenu: NSMenu { NSMenuItem(title: "Personal Information Removal") .submenu(DataBrokerProtectionDebugMenu()) + FreemiumDebugMenu() + if case .normal = NSApp.runType { NSMenuItem(title: "VPN") .submenu(NetworkProtectionDebugMenu()) diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 3cc9e6cfeb..a90a722854 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -60,7 +60,7 @@ final class MoreOptionsMenu: NSMenu { private lazy var sharingMenu: NSMenu = SharingMenu(title: UserText.shareMenuItem) private var accountManager: AccountManager { subscriptionManager.accountManager } private let subscriptionManager: SubscriptionManager - private var freemiumPIRUserState: FreemiumPIRUserState + private var freemiumPIRUserStateManager: FreemiumPIRUserStateManager private let freemiumPIRFeature: FreemiumPIRFeature private let freemiumPIRPresenter: FreemiumPIRPresenter private let appearancePreferences: AppearancePreferences @@ -81,7 +81,7 @@ final class MoreOptionsMenu: NSMenu { sharingMenu: NSMenu? = nil, internalUserDecider: InternalUserDecider, subscriptionManager: SubscriptionManager, - freemiumPIRUserState: FreemiumPIRUserState, + freemiumPIRUserStateManager: FreemiumPIRUserStateManager, freemiumPIRFeature: FreemiumPIRFeature, freemiumPIRPresenter: FreemiumPIRPresenter = DefaultFreemiumPIRPresenter(), appearancePreferences: AppearancePreferences = .shared) { @@ -93,7 +93,7 @@ final class MoreOptionsMenu: NSMenu { self.subscriptionFeatureAvailability = subscriptionFeatureAvailability self.internalUserDecider = internalUserDecider self.subscriptionManager = subscriptionManager - self.freemiumPIRUserState = freemiumPIRUserState + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager self.freemiumPIRFeature = freemiumPIRFeature self.freemiumPIRPresenter = freemiumPIRPresenter self.appearancePreferences = appearancePreferences @@ -272,9 +272,9 @@ final class MoreOptionsMenu: NSMenu { @objc func openFreemiumPIR(_ sender: NSMenuItem) { // TODO: Remove this - freemiumPIRUserState.didOnboard = true + freemiumPIRUserStateManager.didOnboard = true // ------ - freemiumPIRPresenter.showFreemiumPIR(didOnboard: freemiumPIRUserState.didOnboard, windowControllerManager: WindowControllersManager.shared) + freemiumPIRPresenter.showFreemiumPIR(didOnboard: freemiumPIRUserStateManager.didOnboard, windowControllerManager: WindowControllersManager.shared) appearancePreferences.isHomePagePromotionVisible = false } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 7e6a2a0000..ea0df377e4 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -286,14 +286,14 @@ final class NavigationBarViewController: NSViewController { @IBAction func optionsButtonAction(_ sender: NSButton) { let internalUserDecider = NSApp.delegateTyped.internalUserDecider - let freemiumPIRUserState = DefaultFreemiumPIRUserState(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) let freemiumPIRFeature = DefaultFreemiumPIRFeature(subscriptionManager: subscriptionManager, accountManager: subscriptionManager.accountManager) let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: PasswordManagerCoordinator.shared, vpnFeatureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager), internalUserDecider: internalUserDecider, subscriptionManager: subscriptionManager, - freemiumPIRUserState: freemiumPIRUserState, + freemiumPIRUserStateManager: freemiumPIRUserStateManager, freemiumPIRFeature: freemiumPIRFeature) menu.actionDelegate = self diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift index 33c0f09473..21298d2df7 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingConfigMatcherProvider.swift @@ -137,7 +137,7 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr let deprecatedRemoteMessageStorage = DefaultSurveyRemoteMessagingStorage.surveys() - let freemiumPIRUserState = DefaultFreemiumPIRUserState(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) return RemoteMessagingConfigMatcher( appAttributeMatcher: AppAttributeMatcher(statisticsStore: statisticsStore, @@ -164,7 +164,7 @@ final class RemoteMessagingConfigMatcherProvider: RemoteMessagingConfigMatcherPr hasCustomHomePage: startupPreferencesPersistor().launchToCustomHomePage, isDuckPlayerOnboarded: duckPlayerPreferencesPersistor.youtubeOverlayAnyButtonPressed, isDuckPlayerEnabled: duckPlayerPreferencesPersistor.duckPlayerModeBool != false, - isCurrentFreemiumPIRUser: freemiumPIRUserState.isActiveUser, + isCurrentFreemiumPIRUser: freemiumPIRUserStateManager.isActiveUser, dismissedDeprecatedMacRemoteMessageIds: deprecatedRemoteMessageStorage.dismissedMessageIDs() ), percentileStore: RemoteMessagingPercentileUserDefaultsStore(keyValueStore: UserDefaults.standard), diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 272d828485..d4b059e094 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -791,10 +791,10 @@ final class BrowserTabViewController: NSViewController { return homePageViewController ?? { let subscriptionManager = Application.appDelegate.subscriptionManager let freemiumPIRFeature = DefaultFreemiumPIRFeature(subscriptionManager: subscriptionManager, accountManager: subscriptionManager.accountManager) - let freemiumPIRUserState = DefaultFreemiumPIRUserState(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: subscriptionManager.accountManager) let homePageViewController = HomePageViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager, freemiumPIRFeature: freemiumPIRFeature, - freemiumPIRUserState: freemiumPIRUserState) + freemiumPIRUserStateManager: freemiumPIRUserStateManager) self.homePageViewController = homePageViewController return homePageViewController }() diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index df118fecf3..6e45dcc1ac 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -103,7 +103,8 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele authenticationRepository: KeychainAuthenticationData()) let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) - manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager) + manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager, + accountManager: subscriptionManager.accountManager) manager?.agentFinishedLaunching() setupStatusBarMenu() diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 5797267d13..620b5e5d84 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -32,6 +32,7 @@ let package = Package( .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "196.2.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), + .package(path: "../Freemium"), ], targets: [ .target( @@ -42,7 +43,8 @@ let package = Package( .byName(name: "XPCHelper"), .product(name: "PixelKit", package: "BrowserServicesKit"), .product(name: "Configuration", package: "BrowserServicesKit"), - .product(name: "Persistence", package: "BrowserServicesKit") + .product(name: "Persistence", package: "BrowserServicesKit"), + .product(name: "Freemium", package: "Freemium"), ], resources: [.process("Resources")], swiftSettings: [ @@ -54,6 +56,7 @@ let package = Package( dependencies: [ "DataBrokerProtection", "BrowserServicesKit", + "Freemium", ], resources: [ .process("Resources") diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 25d033a053..76e9d63242 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -133,7 +133,8 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { Logger.dataBrokerProtection.error("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! - }) } + }) + } public func runAllOptOuts(showWebView: Bool) { xpc.execute(call: { server in diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index bb9b72f09a..a04ab7d0b4 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -39,7 +39,8 @@ struct DefaultDataBrokerOperationDependencies: DataBrokerOperationDependencies { } enum OperationType { - case scan + case manualScan + case scheduledScan case optOut case all } @@ -120,7 +121,7 @@ class DataBrokerOperation: Operation, @unchecked Sendable { switch operationType { case .optOut: operationsData = brokerProfileQueriesData.flatMap { $0.optOutJobData } - case .scan: + case .manualScan, .scheduledScan: operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanJobData } case .all: operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } @@ -180,7 +181,7 @@ class DataBrokerOperation: Operation, @unchecked Sendable { runner: operationDependencies.runnerProvider.getJobRunner(), pixelHandler: operationDependencies.pixelHandler, showWebView: showWebView, - isImmediateOperation: operationType == .scan, + isImmediateOperation: operationType == .manualScan, userNotificationService: operationDependencies.userNotificationService, shouldRunNextStep: { [weak self] in guard let self = self else { return false } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift index 7720725d85..953b68e824 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerExecutionConfig.swift @@ -26,9 +26,9 @@ public struct DataBrokerExecutionConfig { private let concurrentOperationsOnManualScans: Int = 6 func concurrentOperationsFor(_ operation: OperationType) -> Int { switch operation { - case .all, .optOut: + case .all, .optOut, .scheduledScan: return concurrentOperationsDifferentBrokers - case .scan: + case .manualScan: return concurrentOperationsOnManualScans } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift index 22df56a835..8d2aea83ed 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -23,11 +23,14 @@ import BrowserServicesKit import Configuration import PixelKit import os.log +import Freemium +import Subscription // This is to avoid exposing all the dependancies outside of the DBP package public class DataBrokerProtectionAgentManagerProvider { - public static func agentManager(authenticationManager: DataBrokerProtectionAuthenticationManaging) -> DataBrokerProtectionAgentManager { + public static func agentManager(authenticationManager: DataBrokerProtectionAuthenticationManaging, + accountManager: AccountManager) -> DataBrokerProtectionAgentManager { let pixelHandler = DataBrokerProtectionPixelsHandler() let executionConfig = DataBrokerExecutionConfig() @@ -82,10 +85,13 @@ public class DataBrokerProtectionAgentManagerProvider { emailService: emailService, captchaService: captchaService) + let freemiumPIRUserStateManager = DefaultFreemiumPIRUserStateManager(userDefaults: .dbp, accountManager: accountManager) + let agentstopper = DefaultDataBrokerProtectionAgentStopper(dataManager: dataManager, entitlementMonitor: DataBrokerProtectionEntitlementMonitor(), authenticationManager: authenticationManager, - pixelHandler: pixelHandler) + pixelHandler: pixelHandler, + freemiumPIRUserStateManager: freemiumPIRUserStateManager) let operationDependencies = DefaultDataBrokerOperationDependencies( database: dataManager.database, @@ -105,7 +111,8 @@ public class DataBrokerProtectionAgentManagerProvider { pixelHandler: pixelHandler, agentStopper: agentstopper, configurationManager: configurationManager, - privacyConfigurationManager: privacyConfigurationManager) + privacyConfigurationManager: privacyConfigurationManager), + freemiumPIRUserStateManager: freemiumPIRUserStateManager) } } @@ -121,6 +128,7 @@ public final class DataBrokerProtectionAgentManager { private let agentStopper: DataBrokerProtectionAgentStopper private let configurationManger: DefaultConfigurationManager private let privacyConfigurationManager: DBPPrivacyConfigurationManager + private let freemiumPIRUserStateManager: FreemiumPIRUserStateManager // Used for debug functions only, so not injected private lazy var browserWindowManager = BrowserWindowManager() @@ -138,7 +146,8 @@ public final class DataBrokerProtectionAgentManager { pixelHandler: EventMapping, agentStopper: DataBrokerProtectionAgentStopper, configurationManager: DefaultConfigurationManager, - privacyConfigurationManager: DBPPrivacyConfigurationManager + privacyConfigurationManager: DBPPrivacyConfigurationManager, + freemiumPIRUserStateManager: FreemiumPIRUserStateManager ) { self.userNotificationService = userNotificationService self.activityScheduler = activityScheduler @@ -150,6 +159,7 @@ public final class DataBrokerProtectionAgentManager { self.agentStopper = agentStopper self.configurationManger = configurationManager self.privacyConfigurationManager = privacyConfigurationManager + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager self.activityScheduler.delegate = self self.ipcServer.serverDelegate = self @@ -167,11 +177,11 @@ public final class DataBrokerProtectionAgentManager { activityScheduler.startScheduler() didStartActivityScheduler = true fireMonitoringPixels() - queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil) + startFreemiumOrSubscriptionScheduledOperations(showWebView: false, operationDependencies: operationDependencies, completion: nil) /// Monitors entitlement changes every 60 minutes to optimize system performance and resource utilization by avoiding unnecessary operations when entitlement is invalid. /// While keeping the agent active with invalid entitlement has no significant risk, setting the monitoring interval at 60 minutes is a good balance to minimize backend checks. - agentStopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: .minutes(60)) + agentStopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: .minutes(60)) configurationSubscription = privacyConfigurationManager.updatesPublisher .sink { [weak self] _ in @@ -204,11 +214,29 @@ extension DataBrokerProtectionAgentManager { } } +private extension DataBrokerProtectionAgentManager { + + /// Starts either Freemium (scan-only) or Subscription (scan and opt-out) scheduled operations + /// - Parameters: + /// - showWebView: Whether to show the web view or not + /// - operationDependencies: Operation dependencies + /// - completion: Completion handler + func startFreemiumOrSubscriptionScheduledOperations(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + if freemiumPIRUserStateManager.isActiveUser { + queueManager.startScheduledScanOperationsIfPermitted(showWebView: showWebView, operationDependencies: operationDependencies, completion: completion) + } else { + queueManager.startScheduledAllOperationsIfPermitted(showWebView: showWebView, operationDependencies: operationDependencies, completion: completion) + } + } +} + extension DataBrokerProtectionAgentManager: DataBrokerProtectionBackgroundActivitySchedulerDelegate { public func dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(_ activityScheduler: DataBrokerProtection.DataBrokerProtectionBackgroundActivityScheduler, completion: (() -> Void)?) { fireMonitoringPixels() - queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { _ in + startFreemiumOrSubscriptionScheduledOperations(showWebView: false, operationDependencies: operationDependencies) { _ in completion?() } } @@ -221,7 +249,7 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { userNotificationService.requestNotificationPermission() fireMonitoringPixels() - queueManager.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { [weak self] errors in + queueManager.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies) { [weak self] errors in guard let self = self else { return } if let errors = errors { @@ -257,7 +285,7 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { public func appLaunched() { fireMonitoringPixels() - queueManager.startScheduledOperationsIfPermitted(showWebView: false, + startFreemiumOrSubscriptionScheduledOperations(showWebView: false, operationDependencies: operationDependencies) { [weak self] errors in guard let self = self else { return } @@ -308,13 +336,13 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentDebugComman } public func startImmediateOperations(showWebView: Bool) { - queueManager.startImmediateOperationsIfPermitted(showWebView: showWebView, + queueManager.startImmediateScanOperationsIfPermitted(showWebView: showWebView, operationDependencies: operationDependencies, completion: nil) } public func startScheduledOperations(showWebView: Bool) { - queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, + startFreemiumOrSubscriptionScheduledOperations(showWebView: showWebView, operationDependencies: operationDependencies, completion: nil) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift index dd312afbb1..bbd4f1c774 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -74,12 +74,15 @@ protocol DataBrokerProtectionQueueManager { brokerUpdater: DataBrokerProtectionBrokerUpdater?, pixelHandler: EventMapping) - func startImmediateOperationsIfPermitted(showWebView: Bool, - operationDependencies: DataBrokerOperationDependencies, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) - func startScheduledOperationsIfPermitted(showWebView: Bool, - operationDependencies: DataBrokerOperationDependencies, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startImmediateScanOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startScheduledAllOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startScheduledScanOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) var debugRunningStatusString: String { get } @@ -119,13 +122,13 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa self.pixelHandler = pixelHandler } - func startImmediateOperationsIfPermitted(showWebView: Bool, - operationDependencies: DataBrokerOperationDependencies, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + func startImmediateScanOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { let newMode = DataBrokerProtectionQueueMode.immediate(completion: completion) startOperationsIfPermitted(forNewMode: newMode, - type: .scan, + type: .manualScan, showWebView: showWebView, operationDependencies: operationDependencies) { [weak self] errors in completion?(errors) @@ -133,15 +136,22 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa } } - func startScheduledOperationsIfPermitted(showWebView: Bool, - operationDependencies: DataBrokerOperationDependencies, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { - let newMode = DataBrokerProtectionQueueMode.scheduled(completion: completion) - startOperationsIfPermitted(forNewMode: newMode, - type: .all, - showWebView: showWebView, - operationDependencies: operationDependencies, - completion: completion) + func startScheduledAllOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + startScheduleOperationsIfPermitted(withOperationType: .all, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + + func startScheduledScanOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + startScheduleOperationsIfPermitted(withOperationType: .scheduledScan, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) } func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) { @@ -158,6 +168,18 @@ final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueMa private extension DefaultDataBrokerProtectionQueueManager { + func startScheduleOperationsIfPermitted(withOperationType operationType: OperationType, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + let newMode = DataBrokerProtectionQueueMode.scheduled(completion: completion) + startOperationsIfPermitted(forNewMode: newMode, + type: operationType, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + func startOperationsIfPermitted(forNewMode newMode: DataBrokerProtectionQueueMode, type: OperationType, showWebView: Bool, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift index 747f2b18a6..a7ae9be234 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift @@ -19,14 +19,15 @@ import Foundation import os.log import Common +import Freemium protocol DataBrokerProtectionAgentStopper { - /// Validates if the user has profile data, is authenticated, and has valid entitlement. If any of these conditions are not met, the agent will be stopped. + /// Validates if the user is an active freemium user, OR if they have profile data, is authenticated, and has valid entitlement. If any of these conditions are not met, the agent will be stopped. func validateRunPrerequisitesAndStopAgentIfNecessary() async - /// Monitors the entitlement package. If the entitlement check returns false, the agent will be stopped. + /// Monitors the entitlement package. If the entitlement check returns false, and the user is NOT an active freemium user, the agent will be stopped. /// This function ensures that the agent is stopped if the user's subscription has expired, even if the browser is not active. Regularly checking for entitlement is required since notifications are not posted to agents. - func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) + func monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: TimeInterval) } struct DefaultDataBrokerProtectionAgentStopper: DataBrokerProtectionAgentStopper { @@ -35,44 +36,59 @@ struct DefaultDataBrokerProtectionAgentStopper: DataBrokerProtectionAgentStopper private let authenticationManager: DataBrokerProtectionAuthenticationManaging private let pixelHandler: EventMapping private let stopAction: DataProtectionStopAction + private let freemiumPIRUserStateManager: FreemiumPIRUserStateManager init(dataManager: DataBrokerProtectionDataManaging, entitlementMonitor: DataBrokerProtectionEntitlementMonitoring, authenticationManager: DataBrokerProtectionAuthenticationManaging, pixelHandler: EventMapping, - stopAction: DataProtectionStopAction = DefaultDataProtectionStopAction()) { + stopAction: DataProtectionStopAction = DefaultDataProtectionStopAction(), + freemiumPIRUserStateManager: FreemiumPIRUserStateManager) { self.dataManager = dataManager self.entitlementMonitor = entitlementMonitor self.authenticationManager = authenticationManager self.pixelHandler = pixelHandler self.stopAction = stopAction + self.freemiumPIRUserStateManager = freemiumPIRUserStateManager } + /// Checks PIR prerequisites and stops the agent if necessary + /// + /// Prerequisites are satisified if either: + /// 1. The user is an active freemium user + /// 2. The user has a subscription with valid entitlements public func validateRunPrerequisitesAndStopAgentIfNecessary() async { + do { - guard try dataManager.fetchProfile() != nil, - authenticationManager.isUserAuthenticated else { + let hasProfile = try dataManager.fetchProfile() != nil + let isAuthenticated = authenticationManager.isUserAuthenticated + let isFreemium = freemiumPIRUserStateManager.isActiveUser + + if !hasProfile || (!isAuthenticated && !isFreemium) { Logger.dataBrokerProtection.debug("Prerequisites are invalid") stopAgent() return } - Logger.dataBrokerProtection.debug("Prerequisites are valid") - } catch { - Logger.dataBrokerProtection.error("Error validating prerequisites, error: \(error.localizedDescription, privacy: .public)") - stopAgent() - } - do { - let result = try await authenticationManager.hasValidEntitlement() - stopAgentBasedOnEntitlementCheckResult(result ? .enabled : .disabled) + + if !isAuthenticated && isFreemium { + Logger.dataBrokerProtection.debug("User is Freemium") + return + } + + let hasValidEntitlement = try await authenticationManager.hasValidEntitlement() + stopAgentBasedOnEntitlementCheckResult(hasValidEntitlement ? .enabled : .disabled) + } catch { + Logger.dataBrokerProtection.error("Error validating prerequisites, error: \(error.localizedDescription, privacy: .public)") stopAgentBasedOnEntitlementCheckResult(.error) } } - public func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) { + public func monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: TimeInterval) { entitlementMonitor.start(checkEntitlementFunction: authenticationManager.hasValidEntitlement, interval: interval) { result in + guard !self.freemiumPIRUserStateManager.isActiveUser else { return } stopAgentBasedOnEntitlementCheckResult(result) } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift index cb54af211b..1ed238590a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerExecutionConfigTests.swift @@ -25,11 +25,17 @@ final class DataBrokerExecutionConfigTests: XCTestCase { private let sut = DataBrokerExecutionConfig() func testWhenOperationIsManualScans_thenConcurrentOperationsBetweenBrokersIsSix() { - let value = sut.concurrentOperationsFor(.scan) + let value = sut.concurrentOperationsFor(.manualScan) let expectedValue = 6 XCTAssertEqual(value, expectedValue) } + func testWhenOperationIsScheduledScans_thenConcurrentOperationsBetweenBrokersIsTwo() { + let value = sut.concurrentOperationsFor(.scheduledScan) + let expectedValue = 2 + XCTAssertEqual(value, expectedValue) + } + func testWhenOperationIsAll_thenConcurrentOperationsBetweenBrokersIsTwo() { let value = sut.concurrentOperationsFor(.all) let expectedValue = 2 diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift index a19e16863a..8c839fc327 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift @@ -70,7 +70,7 @@ final class DataBrokerOperationsCreatorTests: XCTestCase { mockDatabase.brokerProfileQueryDataToReturn = dataBrokerProfileQueries // When - let result = try! sut.operations(forOperationType: .scan, + let result = try! sut.operations(forOperationType: .manualScan, withPriorityDate: Date(), showWebView: false, errorDelegate: MockDataBrokerOperationErrorDelegate(), diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift index d0ccaec883..90bfb4046f 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentManagerTests.swift @@ -36,6 +36,7 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { private var mockAgentStopper: MockAgentStopper! private var mockConfigurationManager: MockConfigurationManager! private var mockPrivacyConfigurationManager: DBPPrivacyConfigurationManager! + private var mockFreemiumPIRUserState: MockFreemiumPIRUserState! override func setUpWithError() throws { @@ -72,9 +73,11 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { addresses: [], phones: [], birthYear: 1992) + + mockFreemiumPIRUserState = MockFreemiumPIRUserState() } - func testWhenAgentStart_andProfileExists_thenActivityIsScheduled_andSheduledOpereationsRun() async throws { + func testWhenAgentStart_andProfileExists_andUserIsNotFreemium_thenActivityIsScheduled_andScheduledAllOperationsRun() async throws { // Given sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, @@ -86,9 +89,52 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = false + + let schedulerStartedExpectation = XCTestExpectation(description: "Scheduler started") + var schedulerStarted = false + mockActivityScheduler.startSchedulerCompletion = { + schedulerStarted = true + schedulerStartedExpectation.fulfill() + } + + let scanCalledExpectation = XCTestExpectation(description: "Scan called") + var startScheduledScansCalled = false + mockQueueManager.startScheduledAllOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + scanCalledExpectation.fulfill() + } + + // When + sut.agentFinishedLaunching() + + // Then + await fulfillment(of: [scanCalledExpectation, schedulerStartedExpectation], timeout: 1.0) + XCTAssertTrue(schedulerStarted) + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenAgentStart_andProfileExists_andUserIsFreemium_thenActivityIsScheduled_andScheduledScanOperationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper, + configurationManager: mockConfigurationManager, + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = true let schedulerStartedExpectation = XCTestExpectation(description: "Scheduler started") var schedulerStarted = false @@ -99,7 +145,7 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { let scanCalledExpectation = XCTestExpectation(description: "Scan called") var startScheduledScansCalled = false - mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + mockQueueManager.startScheduledScanOperationsIfPermittedCalledCompletion = { _ in startScheduledScansCalled = true scanCalledExpectation.fulfill() } @@ -113,14 +159,14 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertTrue(startScheduledScansCalled) } - func testWhenAgentStart_andProfileDoesNotExist_thenActivityIsNotScheduled_andStopAgentIsCalled() async throws { + func testWhenAgentStart_andProfileDoesNotExist_andUserIsFreemium_thenActivityIsNotScheduled_andStopAgentIsCalled() async throws { // Given let mockStopAction = MockDataProtectionStopAction() let agentStopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: DataBrokerProtectionEntitlementMonitor(), authenticationManager: MockAuthenticationManager(), pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, freemiumPIRUserStateManager: MockFreemiumPIRUserState()) sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, activityScheduler: mockActivityScheduler, @@ -130,10 +176,10 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { operationDependencies: mockDependencies, pixelHandler: mockPixelHandler, agentStopper: agentStopper, - configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = true let stopAgentExpectation = XCTestExpectation(description: "Stop agent expectation") @@ -165,7 +211,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = nil @@ -192,7 +239,7 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertTrue(monitorEntitlementWasCalled) } - func testWhenActivitySchedulerTriggers_thenSheduledOpereationsRun() async throws { + func testWhenActivitySchedulerTriggers_andUserIsNotFreemium_thenScheduledAllOperationsRun() async throws { // Given sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, @@ -204,12 +251,14 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = false var startScheduledScansCalled = false - mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + mockQueueManager.startScheduledAllOperationsIfPermittedCalledCompletion = { _ in startScheduledScansCalled = true } @@ -220,7 +269,7 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertTrue(startScheduledScansCalled) } - func testWhenProfileSaved_thenImmediateOpereationsRun() async throws { + func testWhenActivitySchedulerTriggers_andUserIsFreemium_thenScheduledScanOperationsRun() async throws { // Given sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, @@ -232,12 +281,70 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = true + + var startScheduledScansCalled = false + mockQueueManager.startScheduledScanOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + mockActivityScheduler.triggerDelegateCall() + + // Then + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenProfileSaved_andUserIsNotFreemium_thenImmediateOperationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = false + + var startImmediateScansCalled = false + mockQueueManager.startImmediateScanOperationsIfPermittedCalledCompletion = { _ in + startImmediateScansCalled = true + } + + // When + sut.profileSaved() + + // Then + XCTAssertTrue(startImmediateScansCalled) + } + + func testWhenProfileSaved_andUserIsFreemium_thenImmediateOperationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockDataManager.profileToReturn = mockProfile + mockFreemiumPIRUserState.isActiveUser = true var startImmediateScansCalled = false - mockQueueManager.startImmediateOperationsIfPermittedCalledCompletion = { _ in + mockQueueManager.startImmediateScanOperationsIfPermittedCalledCompletion = { _ in startImmediateScansCalled = true } @@ -261,6 +368,7 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, privacyConfigurationManager: mockPrivacyConfigurationManager) + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() @@ -283,7 +391,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() @@ -306,10 +415,11 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() - mockQueueManager.startImmediateOperationsIfPermittedCompletionError = DataBrokerProtectionAgentErrorCollection(oneTimeError: NSError(domain: "test", code: 10)) + mockQueueManager.startImmediateScanOperationsIfPermittedCompletionError = DataBrokerProtectionAgentErrorCollection(oneTimeError: NSError(domain: "test", code: 10)) // When sut.profileSaved() @@ -330,7 +440,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() mockDataManager.shouldReturnHasMatches = true @@ -354,7 +465,8 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) mockNotificationService.reset() mockDataManager.shouldReturnHasMatches = false @@ -366,7 +478,7 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { XCTAssertFalse(mockNotificationService.checkInNotificationWasScheduled) } - func testWhenAppLaunched_thenSheduledOpereationsRun() async throws { + func testWhenAppLaunched_andUserIsNotFreemium_thenScheduledAllOperationsRun() async throws { // Given sut = DataBrokerProtectionAgentManager( userNotificationService: mockNotificationService, @@ -378,10 +490,40 @@ final class DataBrokerProtectionAgentManagerTests: XCTestCase { pixelHandler: mockPixelHandler, agentStopper: mockAgentStopper, configurationManager: mockConfigurationManager, - privacyConfigurationManager: mockPrivacyConfigurationManager) + privacyConfigurationManager: mockPrivacyConfigurationManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockFreemiumPIRUserState.isActiveUser = false + + var startScheduledScansCalled = false + mockQueueManager.startScheduledAllOperationsIfPermittedCalledCompletion = { _ in + startScheduledScansCalled = true + } + + // When + sut.appLaunched() + + // Then + XCTAssertTrue(startScheduledScansCalled) + } + + func testWhenAppLaunched_andUserIsFreemium_thenScheduledScanOperationsRun() async throws { + // Given + sut = DataBrokerProtectionAgentManager( + userNotificationService: mockNotificationService, + activityScheduler: mockActivityScheduler, + ipcServer: mockIPCServer, + queueManager: mockQueueManager, + dataManager: mockDataManager, + operationDependencies: mockDependencies, + pixelHandler: mockPixelHandler, + agentStopper: mockAgentStopper, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + mockFreemiumPIRUserState.isActiveUser = true var startScheduledScansCalled = false - mockQueueManager.startScheduledOperationsIfPermittedCalledCompletion = { _ in + mockQueueManager.startScheduledScanOperationsIfPermittedCalledCompletion = { _ in startScheduledScansCalled = true } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift index 3271a31f69..a605d74d6d 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionAgentStopperTests.swift @@ -23,11 +23,13 @@ import Common @testable import DataBrokerProtection final class DataBrokerProtectionAgentStopperTests: XCTestCase { - private var mockPixelHandler: EventMapping! - private var mockAuthenticationManager: MockAuthenticationManager! - private var mockEntitlementMonitor: DataBrokerProtectionEntitlementMonitor! - private var mockDataManager: MockDataBrokerProtectionDataManager! - private var mockStopAction: MockDataProtectionStopAction! + + private var mockPixelHandler: EventMapping! + private var mockAuthenticationManager: MockAuthenticationManager! + private var mockEntitlementMonitor: DataBrokerProtectionEntitlementMonitor! + private var mockDataManager: MockDataBrokerProtectionDataManager! + private var mockStopAction: MockDataProtectionStopAction! + private var mockFreemiumPIRUserState: MockFreemiumPIRUserState! private var fakeProfile: DataBrokerProtectionProfile { let name = DataBrokerProtectionProfile.Name(firstName: "John", lastName: "Doe") @@ -44,6 +46,8 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { mockDataManager = MockDataBrokerProtectionDataManager(pixelHandler: mockPixelHandler, fakeBrokerFlag: DataBrokerDebugFlagFakeBroker()) mockStopAction = MockDataProtectionStopAction() + mockFreemiumPIRUserState = MockFreemiumPIRUserState() + mockFreemiumPIRUserState.isActiveUser = false } override func tearDown() { @@ -55,51 +59,176 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { mockStopAction = nil } - func testNoProfile_thenStopAgentIsCalled() async { + func testNoProfile_andUserIsNotAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = false + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testNoProfile_andUserIsNotAuthenticated_andUserIsFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testNoProfile_andUserIsAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = true mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertTrue(mockStopAction.wasStopCalled) } - func testInvalidEntitlement_thenStopAgentIsCalled() async { + func testNoProfile_andUserIsAuthenticated_andUserIsFreemium_thenStopAgentIsCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = nil + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testInvalidEntitlement_andUserIsNotAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false mockAuthenticationManager.hasValidEntitlementValue = false mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertTrue(mockStopAction.wasStopCalled) } - func testUserNotAuthenticated_thenStopAgentIsCalled() async { + func testInvalidEntitlement_andUserIsNotAuthenticated_andUserIsFreemium_thenStopAgentIsNotCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertFalse(mockStopAction.wasStopCalled) + } + + func testInvalidEntitlement_andUserIsAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testInvalidEntitlement_andUserIsAuthenticated_andUserIsFreemium_thenStopAgentIsCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertTrue(mockStopAction.wasStopCalled) + } + + func testUserNotAuthenticated_andUserIsNotFreemium_thenStopAgentIsCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = false mockAuthenticationManager.hasValidEntitlementValue = true mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertTrue(mockStopAction.wasStopCalled) } + func testUserNotAuthenticated_andUserIsFreemium_thenStopAgentIsNotCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = false + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertFalse(mockStopAction.wasStopCalled) + } + func testErrorEntitlement_thenStopAgentIsNotCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.shouldThrowEntitlementError = true @@ -109,40 +238,62 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() + + XCTAssertFalse(mockStopAction.wasStopCalled) + } + + func testValidEntitlement_andUserIsNotFreemium_thenStopAgentIsNotCalled() async { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertFalse(mockStopAction.wasStopCalled) } - func testValidEntitlement_thenStopAgentIsNotCalled() async { + func testValidEntitlement_andUserIsFreemium_thenStopAgentIsNotCalled() async { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = true mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) await stopper.validateRunPrerequisitesAndStopAgentIfNecessary() XCTAssertFalse(mockStopAction.wasStopCalled) } - func testEntitlementMonitorWithValidResult_thenStopAgentIsNotCalled() { + func testEntitlementMonitorWithValidResult_andUserIsNotFreemium_thenStopAgentIsNotCalled() { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = true mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) let expectation = XCTestExpectation(description: "Wait for monitor") - stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertFalse(mockStopAction.wasStopCalled) @@ -152,19 +303,45 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { wait(for: [expectation], timeout: 3) } - func testEntitlementMonitorWithInValidResult_thenStopAgentIsCalled() { + func testEntitlementMonitorWithValidResult_andUserIsFreemium_thenStopAgentIsNotCalled() { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = true + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + let expectation = XCTestExpectation(description: "Wait for monitor") + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in + XCTAssertFalse(mockStopAction.wasStopCalled) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + } + + func testEntitlementMonitorWithInValidResult_andUserIsNotFreemium_thenStopAgentIsCalled() { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.hasValidEntitlementValue = false mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = false let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) let expectation = XCTestExpectation(description: "Wait for monitor") - stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertTrue(mockStopAction.wasStopCalled) @@ -174,6 +351,30 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { wait(for: [expectation], timeout: 3) } + func testEntitlementMonitorWithInValidResult_andUserIsFreemium_thenStopAgentIsNotCalled() { + mockAuthenticationManager.isUserAuthenticatedValue = true + mockAuthenticationManager.hasValidEntitlementValue = false + mockDataManager.profileToReturn = fakeProfile + mockFreemiumPIRUserState.isActiveUser = true + + let stopper = DefaultDataBrokerProtectionAgentStopper(dataManager: mockDataManager, + entitlementMonitor: mockEntitlementMonitor, + authenticationManager: mockAuthenticationManager, + pixelHandler: mockPixelHandler, + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + let expectation = XCTestExpectation(description: "Wait for monitor") + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in + XCTAssertFalse(mockStopAction.wasStopCalled) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 3) + } + func testEntitlementMonitorWithErrorResult_thenStopAgentIsNotCalled() { mockAuthenticationManager.isUserAuthenticatedValue = true mockAuthenticationManager.shouldThrowEntitlementError = true @@ -183,10 +384,11 @@ final class DataBrokerProtectionAgentStopperTests: XCTestCase { entitlementMonitor: mockEntitlementMonitor, authenticationManager: mockAuthenticationManager, pixelHandler: mockPixelHandler, - stopAction: mockStopAction) + stopAction: mockStopAction, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) let expectation = XCTestExpectation(description: "Wait for monitor") - stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: 0.1) + stopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: 0.1) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in XCTAssertFalse(mockStopAction.wasStopCalled) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift index e69d984c64..592f6c88a9 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift @@ -53,6 +53,57 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { userNotificationService: mockUserNotification) } + func testWhenStartImmediateScanOperations_thenCreatorIsCalledWithManualScanOperationType() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + + // When + sut.startImmediateScanOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies, + completion: nil) + + // Then + XCTAssertEqual(mockOperationsCreator.createdType, .manualScan) + } + + func testWhenStartScheduledAllOperations_thenCreatorIsCalledWithAllOperationType() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + + // When + sut.startScheduledAllOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies, + completion: nil) + + // Then + XCTAssertEqual(mockOperationsCreator.createdType, .all) + } + + func testWhenStartScheduledScanOperations_thenCreatorIsCalledWithScheduledScanOperationType() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + + // When + sut.startScheduledScanOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies, + completion: nil) + + // Then + XCTAssertEqual(mockOperationsCreator.createdType, .scheduledScan) + } + func testWhenStartImmediateScan_andScanCompletesWithErrors_thenCompletionIsCalledWithErrors() async throws { // Given sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, @@ -60,15 +111,15 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scan, errorDelegate: sut) - let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scan, errorDelegate: sut, shouldError: true) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .manualScan, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .manualScan, errorDelegate: sut, shouldError: true) mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] let expectation = expectation(description: "Expected errors to be returned in completion") var errorCollection: DataBrokerProtectionAgentErrorCollection! - let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.scan) + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.manualScan) // When - sut.startImmediateOperationsIfPermitted(showWebView: false, + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors expectation.fulfill() @@ -83,22 +134,52 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) } - func testWhenStartScheduledScan_andScanCompletesWithErrors_thenCompletionIsCalledWithErrors() async throws { + func testWhenStartScheduledAllOperations_andOperationsCompleteWithErrors_thenCompletionIsCalledWithErrors() async throws { // Given sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, operationsCreator: mockOperationsCreator, mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scan, errorDelegate: sut) - let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scan, errorDelegate: sut, shouldError: true) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .all, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .all, errorDelegate: sut, shouldError: true) mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] let expectation = expectation(description: "Expected errors to be returned in completion") var errorCollection: DataBrokerProtectionAgentErrorCollection! let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.all) // When - sut.startScheduledOperationsIfPermitted(showWebView: false, + sut.startScheduledAllOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 5) + XCTAssert(errorCollection.operationErrors?.count == 1) + XCTAssertNotNil(mockOperationsCreator.priorityDate) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } + + func testWhenStartScheduledScanOperations_andOperationsCompleteWithErrors_thenCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperation = MockDataBrokerOperation(id: 1, operationType: .scheduledScan, errorDelegate: sut) + let mockOperationWithError = MockDataBrokerOperation(id: 2, operationType: .scheduledScan, errorDelegate: sut, shouldError: true) + mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] + let expectation = expectation(description: "Expected errors to be returned in completion") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.scheduledScan) + + // When + sut.startScheduledScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors expectation.fulfill() @@ -120,13 +201,13 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } - var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations var errorCollection: DataBrokerProtectionAgentErrorCollection! // When - sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startScheduledAllOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors } @@ -136,11 +217,11 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssert(mockQueue.operationCount == 2) // Given - mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperations // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } // Then XCTAssert(errorCollection.operationErrors?.count == 2) @@ -158,13 +239,13 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } - var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations var errorCollection: DataBrokerProtectionAgentErrorCollection! // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors } @@ -174,11 +255,11 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssert(mockQueue.operationCount == 2) // Given - mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperations // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } // Then XCTAssert(errorCollection.operationErrors?.count == 2) @@ -196,13 +277,13 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - var mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } - var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + var mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut, shouldError: true) } + var mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations var errorCollectionFirst: DataBrokerProtectionAgentErrorCollection! // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollectionFirst = errors } @@ -213,12 +294,12 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { // Given var errorCollectionSecond: DataBrokerProtectionAgentErrorCollection! - mockOperationsWithError = (5...6).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut, shouldError: true) } - mockOperations = (7...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsWithError = (5...6).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut, shouldError: true) } + mockOperations = (7...8).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollectionSecond = errors } @@ -230,31 +311,76 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { XCTAssert(mockQueue.didCallCancelCount == 1) } - func testWhenStartScheduledScan_andCurrentModeIsImmediate_thenCurrentOperationsAreNotInterrupted_andNewCompletionIsCalledWithError() throws { + func testWhenStartScheduledAllOperations_andCurrentModeIsImmediate_thenCurrentOperationsAreNotInterrupted_andNewCompletionIsCalledWithError() throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + var mockOperations = (1...5).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } + var mockOperationsWithError = (6...10).map { MockDataBrokerOperation(id: $0, + operationType: .manualScan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(mockQueue.operationCount == 10) + + // Given + mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } + mockOperationsWithError = (16...20).map { MockDataBrokerOperation(id: $0, + operationType: .manualScan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + let expectedError = DataBrokerProtectionQueueError.cannotInterrupt + var completionCalled = false + + // When + sut.startScheduledAllOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + completionCalled.toggle() + } + + // Then + XCTAssert(mockQueue.didCallCancelCount == 0) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 10) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count == 0) + XCTAssertEqual((errorCollection.oneTimeError as? DataBrokerProtectionQueueError), expectedError) + XCTAssert(completionCalled) + } + + func testWhenStartScheduledScanOperations_andCurrentModeIsImmediate_thenCurrentOperationsAreNotInterrupted_andNewCompletionIsCalledWithError() throws { // Given sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, operationsCreator: mockOperationsCreator, mismatchCalculator: mockMismatchCalculator, brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) - var mockOperations = (1...5).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + var mockOperations = (1...5).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } var mockOperationsWithError = (6...10).map { MockDataBrokerOperation(id: $0, - operationType: .scan, + operationType: .manualScan, errorDelegate: sut, shouldError: true) } mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError var errorCollection: DataBrokerProtectionAgentErrorCollection! // When - sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } // Then XCTAssert(mockQueue.operationCount == 10) // Given - mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .manualScan, errorDelegate: sut) } mockOperationsWithError = (16...20).map { MockDataBrokerOperation(id: $0, - operationType: .scan, + operationType: .manualScan, errorDelegate: sut, shouldError: true) } mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError @@ -262,7 +388,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { var completionCalled = false // When - sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + sut.startScheduledScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors completionCalled.toggle() } @@ -287,7 +413,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { var errorCollection: DataBrokerProtectionAgentErrorCollection! // When - sut.startImmediateOperationsIfPermitted(showWebView: false, + sut.startImmediateScanOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in errorCollection = errors expectation.fulfill() @@ -306,7 +432,7 @@ final class DataBrokerProtectionQueueManagerTests: XCTestCase { brokerUpdater: mockUpdater, pixelHandler: mockPixelHandler) let expectedConcurrentOperations = DataBrokerExecutionConfig().concurrentOperationsFor(.optOut) - XCTAssert(mockOperationsCreator.createdType == .scan) + XCTAssert(mockOperationsCreator.createdType == .manualScan) // When sut.execute(.startOptOutOperations(showWebView: false, diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 7eda40ac91..66d738efb6 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -23,6 +23,7 @@ import Configuration import Foundation import GRDB import SecureStorage +import Freemium @testable import DataBrokerProtection @@ -1192,24 +1193,31 @@ extension DataBroker { final class MockDataBrokerProtectionOperationQueueManager: DataBrokerProtectionQueueManager { var debugRunningStatusString: String { return "" } - var startImmediateOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? - var startScheduledOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + var startImmediateScanOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + var startScheduledAllOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? + var startScheduledScanOperationsIfPermittedCompletionError: DataBrokerProtectionAgentErrorCollection? - var startImmediateOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? - var startScheduledOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + var startImmediateScanOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + var startScheduledAllOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? + var startScheduledScanOperationsIfPermittedCalledCompletion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)? init(operationQueue: DataBrokerProtection.DataBrokerProtectionOperationQueue, operationsCreator: DataBrokerProtection.DataBrokerOperationsCreator, mismatchCalculator: DataBrokerProtection.MismatchCalculator, brokerUpdater: DataBrokerProtection.DataBrokerProtectionBrokerUpdater?, pixelHandler: Common.EventMapping) { } - func startImmediateOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { - completion?(startImmediateOperationsIfPermittedCompletionError) - startImmediateOperationsIfPermittedCalledCompletion?(startImmediateOperationsIfPermittedCompletionError) + func startImmediateScanOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startImmediateScanOperationsIfPermittedCompletionError) + startImmediateScanOperationsIfPermittedCalledCompletion?(startImmediateScanOperationsIfPermittedCompletionError) } - func startScheduledOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { - completion?(startScheduledOperationsIfPermittedCompletionError) - startScheduledOperationsIfPermittedCalledCompletion?(startScheduledOperationsIfPermittedCompletionError) + func startScheduledAllOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startScheduledAllOperationsIfPermittedCompletionError) + startScheduledAllOperationsIfPermittedCalledCompletion?(startScheduledAllOperationsIfPermittedCompletionError) + } + + func startScheduledScanOperationsIfPermitted(showWebView: Bool, operationDependencies: DataBrokerProtection.DataBrokerOperationDependencies, completion: ((DataBrokerProtection.DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + completion?(startScheduledScanOperationsIfPermittedCompletionError) + startScheduledScanOperationsIfPermittedCalledCompletion?(startScheduledScanOperationsIfPermittedCompletionError) } func execute(_ command: DataBrokerProtection.DataBrokerProtectionQueueManagerDebugCommand) { @@ -1484,7 +1492,7 @@ final class MockDataBrokerOperationsCreator: DataBrokerOperationsCreator { var operationCollections: [DataBrokerOperation] = [] var shouldError = false var priorityDate: Date? - var createdType: OperationType = .scan + var createdType: OperationType = .manualScan init(operationCollections: [DataBrokerOperation] = []) { self.operationCollections = operationCollections @@ -1578,7 +1586,7 @@ final class MockAgentStopper: DataBrokerProtectionAgentStopper { validateRunPrerequisitesCompletion?() } - func monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: TimeInterval) { + func monitorEntitlementAndStopAgentIfEntitlementIsInvalidAndUserIsNotFreemium(interval: TimeInterval) { monitorEntitlementCompletion?() } } @@ -1932,3 +1940,8 @@ struct MockMigrationsProvider: DataBrokerProtectionDatabaseMigrationsProvider { return { _ in } } } + +final class MockFreemiumPIRUserState: FreemiumPIRUserStateManager { + var didOnboard = false + var isActiveUser = false +} diff --git a/LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserState.swift b/LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserStateManager.swift similarity index 82% rename from LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserState.swift rename to LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserStateManager.swift index 8a2ea1443a..315eab91b2 100644 --- a/LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserState.swift +++ b/LocalPackages/Freemium/Sources/Freemium/FreemiumPIRUserStateManager.swift @@ -1,5 +1,5 @@ // -// FreemiumPIRUserState.swift +// FreemiumPIRUserStateManager.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -19,14 +19,16 @@ import Foundation import Subscription -/// `FreemiumPIRUserState` types provide access to Freemium PIR-related state -public protocol FreemiumPIRUserState { +/// `FreemiumPIRUserStateManager` types provide access to Freemium PIR-related state +public protocol FreemiumPIRUserStateManager { var didOnboard: Bool { get set } + + /// `isActiveUser` implementations`should only return `true` if the current user DOES NOT have a subscription var isActiveUser: Bool { get } } -/// Default implementation of `FreemiumPIRUserState`. `UserDefaults` is used as underlying storage. -public final class DefaultFreemiumPIRUserState: FreemiumPIRUserState { +/// Default implementation of `FreemiumPIRUserStateManager`. `UserDefaults` is used as underlying storage. +public final class DefaultFreemiumPIRUserStateManager: FreemiumPIRUserStateManager { private enum Keys { static let didOnboard = "macos.browser.freemium.pir.did.onboard" diff --git a/LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateTests.swift b/LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateManagerTests.swift similarity index 79% rename from LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateTests.swift rename to LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateManagerTests.swift index c8543e0269..d17f2e0676 100644 --- a/LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateTests.swift +++ b/LocalPackages/Freemium/Tests/FreemiumTests/FreemiumPIRUserStateManagerTests.swift @@ -1,5 +1,5 @@ // -// FreemiumPIRUserStateTests.swift +// FreemiumPIRUserStateManagerTests.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -21,21 +21,21 @@ import XCTest import Subscription import SubscriptionTestingUtilities -final class FreemiumPIRUserStateTests: XCTestCase { +final class FreemiumPIRUserStateManagerTests: XCTestCase { private static let testSuiteName = "test.defaults.freemium.user.state.tests" private let pir = "macos.browser.freemium.pir.did.onboard" - private let testUserDefaults = UserDefaults(suiteName: FreemiumPIRUserStateTests.testSuiteName)! + private let testUserDefaults = UserDefaults(suiteName: FreemiumPIRUserStateManagerTests.testSuiteName)! private var mockAccountManager: AccountManagerMock! override func setUpWithError() throws { mockAccountManager = AccountManagerMock() - testUserDefaults.removePersistentDomain(forName: FreemiumPIRUserStateTests.testSuiteName) + testUserDefaults.removePersistentDomain(forName: FreemiumPIRUserStateManagerTests.testSuiteName) } func testSetsHasFreemiumPIR() throws { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(testUserDefaults.bool(forKey: pir)) // When @@ -47,7 +47,7 @@ final class FreemiumPIRUserStateTests: XCTestCase { func testGetsHasFreemiumPIR() throws { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(sut.didOnboard) testUserDefaults.setValue(true, forKey: pir) XCTAssertTrue(testUserDefaults.bool(forKey: pir)) @@ -61,7 +61,7 @@ final class FreemiumPIRUserStateTests: XCTestCase { func testIsCurrentFreemiumPIRUser_WhenDidOnboardIsTrueAndUserIsNotAuthenticated_ShouldReturnTrue() { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(sut.didOnboard) testUserDefaults.setValue(true, forKey: pir) mockAccountManager.accessToken = nil @@ -76,7 +76,7 @@ final class FreemiumPIRUserStateTests: XCTestCase { func testIsCurrentFreemiumPIRUser_WhenDidOnboardIsTrueAndUserIsAuthenticated_ShouldReturnFalse() { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(sut.didOnboard) testUserDefaults.setValue(true, forKey: pir) mockAccountManager.accessToken = "some_token" @@ -91,7 +91,7 @@ final class FreemiumPIRUserStateTests: XCTestCase { func testIsCurrentFreemiumPIRUser_WhenDidOnboardIsFalse_ShouldReturnFalse() { // Given - let sut = DefaultFreemiumPIRUserState(userDefaults: testUserDefaults, accountManager: mockAccountManager) + let sut = DefaultFreemiumPIRUserStateManager(userDefaults: testUserDefaults, accountManager: mockAccountManager) XCTAssertFalse(sut.didOnboard) testUserDefaults.setValue(false, forKey: pir) mockAccountManager.accessToken = "some_token" diff --git a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift index b64e193a4b..2d43206eca 100644 --- a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift +++ b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift @@ -28,6 +28,7 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { private var mockFeatureDisabler: MockFeatureDisabler! private var mockFeatureAvailability: MockFeatureAvailability! private var mockAccountManager: MockAccountManager! + private var mockFreemiumPIRUserState: MockFreemiumPIRUserState! private func userDefaults() -> UserDefaults { UserDefaults(suiteName: "testing_\(UUID().uuidString)")! @@ -37,16 +38,19 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { mockFeatureDisabler = MockFeatureDisabler() mockFeatureAvailability = MockFeatureAvailability() mockAccountManager = MockAccountManager() + mockFreemiumPIRUserState = MockFreemiumPIRUserState() + mockFreemiumPIRUserState.isActiveUser = false } - func testWhenNoAccessTokenIsFound_butEntitlementIs_thenFeatureIsDisabled() async { + func testWhenNoAccessTokenIsFound_butEntitlementIs_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given mockAccountManager.accessToken = nil mockAccountManager.hasEntitlementResult = .success(true) sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager) + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) // When let result = await sut.arePrerequisitesSatisfied() @@ -55,14 +59,16 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { XCTAssertFalse(result) } - func testWhenAccessTokenIsFound_butNoEntitlementIs_thenFeatureIsDisabled() async { + func testWhenAccessTokenIsFound_butNoEntitlementIs_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given mockAccountManager.accessToken = "token" mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockFreemiumPIRUserState.isActiveUser = false sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager) + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) // When let result = await sut.arePrerequisitesSatisfied() @@ -71,14 +77,34 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { XCTAssertFalse(result) } - func testWhenAccessTokenAndEntitlementAreNotFound_thenFeatureIsDisabled() async { + func testWhenAccessTokenIsFound_butNoEntitlementIs_andIsActiveFreemiumUser_thenFeatureIsDisabled() async { + // Given + mockAccountManager.accessToken = "token" + mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockFreemiumPIRUserState.isActiveUser = true + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertTrue(result) + } + + func testWhenAccessTokenAndEntitlementAreNotFound_andIsNotActiveFreemiumUser_thenFeatureIsDisabled() async { // Given mockAccountManager.accessToken = nil mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockFreemiumPIRUserState.isActiveUser = false sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager) + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) // When let result = await sut.arePrerequisitesSatisfied() @@ -87,14 +113,34 @@ final class DataBrokerProtectionFeatureGatekeeperTests: XCTestCase { XCTAssertFalse(result) } - func testWhenAccessTokenAndEntitlementAreFound_thenFeatureIsEnabled() async { + func testWhenAccessTokenAndEntitlementAreFound_andIsNotActiveFreemiumUser_thenFeatureIsEnabled() async { // Given mockAccountManager.accessToken = "token" mockAccountManager.hasEntitlementResult = .success(true) + mockFreemiumPIRUserState.isActiveUser = false + sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, + userDefaults: userDefaults(), + subscriptionAvailability: mockFeatureAvailability, + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) + + // When + let result = await sut.arePrerequisitesSatisfied() + + // Then + XCTAssertTrue(result) + } + + func testWhenAccessTokenAndEntitlementAreNotFound_andIsActiveFreemiumUser_thenFeatureIsEnabled() async { + // Given + mockAccountManager.accessToken = nil + mockAccountManager.hasEntitlementResult = .failure(MockError.someError) + mockFreemiumPIRUserState.isActiveUser = true sut = DefaultDataBrokerProtectionFeatureGatekeeper(featureDisabler: mockFeatureDisabler, userDefaults: userDefaults(), subscriptionAvailability: mockFeatureAvailability, - accountManager: mockAccountManager) + accountManager: mockAccountManager, + freemiumPIRUserStateManager: mockFreemiumPIRUserState) // When let result = await sut.arePrerequisitesSatisfied() diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 50bf47d74a..710b5dad07 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -89,7 +89,7 @@ final class MoreOptionsMenuTests: XCTestCase { sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, subscriptionManager: subscriptionManager, - freemiumPIRUserState: mockFreemiumPIRUserState, + freemiumPIRUserStateManager: mockFreemiumPIRUserState, freemiumPIRFeature: freemiumPIRFeature, freemiumPIRPresenter: mockFreemiumPIRPresenter) diff --git a/UnitTests/RemoteMessaging/RemoteMessagingClientTests.swift b/UnitTests/RemoteMessaging/RemoteMessagingClientTests.swift index f6e357c572..2a8ed992bf 100644 --- a/UnitTests/RemoteMessaging/RemoteMessagingClientTests.swift +++ b/UnitTests/RemoteMessaging/RemoteMessagingClientTests.swift @@ -30,7 +30,7 @@ struct MockRemoteMessagingStoreProvider: RemoteMessagingStoreProviding { } } -struct MockFreemiumPIRUserState: FreemiumPIRUserState { +struct MockFreemiumPIRUserState: FreemiumPIRUserStateManager { var didOnboard = false var isActiveUser = false }