diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 5882131727..e34bac501c 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -105,7 +105,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Operations") { NSMenuItem(title: "Hidden WebView") { menuItem(withTitle: "Run queued operations", - action: #selector(DataBrokerProtectionDebugMenu.runQueuedOperations(_:)), + action: #selector(DataBrokerProtectionDebugMenu.startScheduledOperations(_:)), representedObject: false) menuItem(withTitle: "Run scan operations", @@ -119,7 +119,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { NSMenuItem(title: "Visible WebView") { menuItem(withTitle: "Run queued operations", - action: #selector(DataBrokerProtectionDebugMenu.runQueuedOperations(_:)), + action: #selector(DataBrokerProtectionDebugMenu.startScheduledOperations(_:)), representedObject: true) menuItem(withTitle: "Run scan operations", @@ -204,18 +204,18 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } } - @objc private func runQueuedOperations(_ sender: NSMenuItem) { + @objc private func startScheduledOperations(_ sender: NSMenuItem) { os_log("Running queued operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.loginItemInterface.runQueuedOperations(showWebView: showWebView) + DataBrokerProtectionManager.shared.loginItemInterface.startScheduledOperations(showWebView: showWebView) } @objc private func runScanOperations(_ sender: NSMenuItem) { os_log("Running scan operations...", log: .dataBrokerProtection) let showWebView = sender.representedObject as? Bool ?? false - DataBrokerProtectionManager.shared.loginItemInterface.startManualScan(showWebView: showWebView) + DataBrokerProtectionManager.shared.loginItemInterface.startImmediateOperations(showWebView: showWebView) } @objc private func runOptoutOperations(_ sender: NSMenuItem) { @@ -295,7 +295,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func forceBrokerJSONFilesUpdate() { - if let updater = DataBrokerProtectionBrokerUpdater.provide() { + if let updater = DefaultDataBrokerProtectionBrokerUpdater.provideForDebug() { updater.updateBrokers() } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift index 87652a1aa6..1ff520cac3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift @@ -81,12 +81,12 @@ extension DefaultDataBrokerProtectionLoginItemInterface: DataBrokerProtectionLog ipcClient.openBrowser(domain: domain) } - func startManualScan(showWebView: Bool) { - ipcClient.startManualScan(showWebView: showWebView) + func startImmediateOperations(showWebView: Bool) { + ipcClient.startImmediateOperations(showWebView: showWebView) } - func runQueuedOperations(showWebView: Bool) { - ipcClient.runQueuedOperations(showWebView: showWebView) + func startScheduledOperations(showWebView: Bool) { + ipcClient.startScheduledOperations(showWebView: showWebView) } func runAllOptOuts(showWebView: Bool) { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift index 2f91fe97a0..37f9f41e29 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift @@ -39,7 +39,7 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double { 0.0 diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift index 3ef3bfc69f..392508440a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionAgentInterface.swift @@ -70,8 +70,8 @@ public protocol DataBrokerProtectionAgentAppEvents { public protocol DataBrokerProtectionAgentDebugCommands { func openBrowser(domain: String) - func startManualScan(showWebView: Bool) - func runQueuedOperations(showWebView: Bool) + func startImmediateOperations(showWebView: Bool) + func startScheduledOperations(showWebView: Bool) func runAllOptOuts(showWebView: Bool) func getDebugMetadata() async -> DBPBackgroundAgentMetadata? } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 564e33b1b5..9f8b565bc1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -133,9 +133,9 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func startManualScan(showWebView: Bool) { + public func startImmediateOperations(showWebView: Bool) { xpc.execute(call: { server in - server.startManualScan(showWebView: showWebView) + server.startImmediateOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block @@ -143,9 +143,9 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { }) } - public func runQueuedOperations(showWebView: Bool) { + public func startScheduledOperations(showWebView: Bool) { xpc.execute(call: { server in - server.runQueuedOperations(showWebView: showWebView) + server.startScheduledOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in os_log("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift index 53e260cdcf..7213f09986 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCServer.swift @@ -114,8 +114,8 @@ protocol XPCServerInterface { /// func openBrowser(domain: String) - func startManualScan(showWebView: Bool) - func runQueuedOperations(showWebView: Bool) + func startImmediateOperations(showWebView: Bool) + func startScheduledOperations(showWebView: Bool) func runAllOptOuts(showWebView: Bool) func getDebugMetadata(completion: @escaping (DBPBackgroundAgentMetadata?) -> Void) } @@ -180,12 +180,12 @@ extension DataBrokerProtectionIPCServer: XPCServerInterface { serverDelegate?.openBrowser(domain: domain) } - func startManualScan(showWebView: Bool) { - serverDelegate?.startManualScan(showWebView: showWebView) + func startImmediateOperations(showWebView: Bool) { + serverDelegate?.startImmediateOperations(showWebView: showWebView) } - func runQueuedOperations(showWebView: Bool) { - serverDelegate?.runQueuedOperations(showWebView: showWebView) + func startScheduledOperations(showWebView: Bool) { + serverDelegate?.startScheduledOperations(showWebView: showWebView) } func runAllOptOuts(showWebView: Bool) { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift index 993b02aa87..1be8f94200 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJob.swift @@ -199,7 +199,7 @@ extension DataBrokerJob { } private func fireSiteLoadingPixel(startTime: Date, hasError: Bool) { - if stageCalculator.isManualScan { + if stageCalculator.isImmediateOperation { let dataBrokerURL = self.query.dataBroker.url let durationInMs = (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero) pixelHandler.fire(.initialScanSiteLoadDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) @@ -207,7 +207,7 @@ extension DataBrokerJob { } func firePostLoadingDurationPixel(hasError: Bool) { - if stageCalculator.isManualScan, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { + if stageCalculator.isImmediateOperation, let postLoadingSiteStartTime = self.postLoadingSiteStartTime { let dataBrokerURL = self.query.dataBroker.url let durationInMs = (Date().timeIntervalSince(postLoadingSiteStartTime) * 1000).rounded(.towardZero) pixelHandler.fire(.initialScanPostLoadingDuration(duration: durationInMs, hasError: hasError, brokerURL: dataBrokerURL)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index c5032a2df3..8d76634d42 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -19,15 +19,9 @@ import Foundation import Common -enum OperationType { - case manualScan - case optOut - case all -} - protocol DataBrokerOperationDependencies { var database: DataBrokerProtectionRepository { get } - var brokerTimeInterval: TimeInterval { get } + var config: DataBrokerProtectionProcessorConfiguration { get } var runnerProvider: JobRunnerProvider { get } var notificationCenter: NotificationCenter { get } var pixelHandler: EventMapping { get } @@ -36,30 +30,36 @@ protocol DataBrokerOperationDependencies { struct DefaultDataBrokerOperationDependencies: DataBrokerOperationDependencies { let database: DataBrokerProtectionRepository - let brokerTimeInterval: TimeInterval + var config: DataBrokerProtectionProcessorConfiguration let runnerProvider: JobRunnerProvider let notificationCenter: NotificationCenter let pixelHandler: EventMapping let userNotificationService: DataBrokerProtectionUserNotificationService } -final class DataBrokerOperation: Operation { +enum OperationType { + case scan + case optOut + case all +} + +protocol DataBrokerOperationErrorDelegate: AnyObject { + func dataBrokerOperationDidError(_ error: Error, withBrokerName brokerName: String?) +} - public var error: Error? +// swiftlint:disable explicit_non_final_class +class DataBrokerOperation: Operation { private let dataBrokerID: Int64 - private let database: DataBrokerProtectionRepository + private let operationType: OperationType + private let priorityDate: Date? // The date to filter and sort operations priorities + private let showWebView: Bool + private(set) weak var errorDelegate: DataBrokerOperationErrorDelegate? // Internal read-only to enable mocking + private let operationDependencies: DataBrokerOperationDependencies + private let id = UUID() private var _isExecuting = false private var _isFinished = false - private let brokerTimeInterval: TimeInterval? // The time in seconds to wait in-between operations - private let priorityDate: Date? // The date to filter and sort operations priorities - private let operationType: OperationType - private let notificationCenter: NotificationCenter - private let runner: WebJobRunner - private let pixelHandler: EventMapping - private let showWebView: Bool - private let userNotificationService: DataBrokerProtectionUserNotificationService deinit { os_log("Deinit operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) @@ -69,18 +69,15 @@ final class DataBrokerOperation: Operation { operationType: OperationType, priorityDate: Date? = nil, showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, operationDependencies: DataBrokerOperationDependencies) { self.dataBrokerID = dataBrokerID self.priorityDate = priorityDate self.operationType = operationType self.showWebView = showWebView - self.database = operationDependencies.database - self.brokerTimeInterval = operationDependencies.brokerTimeInterval - self.runner = operationDependencies.runnerProvider.getJobRunner() - self.notificationCenter = operationDependencies.notificationCenter - self.pixelHandler = operationDependencies.pixelHandler - self.userNotificationService = operationDependencies.userNotificationService + self.errorDelegate = errorDelegate + self.operationDependencies = operationDependencies super.init() } @@ -122,7 +119,7 @@ final class DataBrokerOperation: Operation { switch operationType { case .optOut: operationsData = brokerProfileQueriesData.flatMap { $0.optOutJobData } - case .manualScan: + case .scan: operationsData = brokerProfileQueriesData.filter { $0.profileQuery.deprecated == false }.compactMap { $0.scanJobData } case .all: operationsData = brokerProfileQueriesData.flatMap { $0.operationsData } @@ -141,12 +138,11 @@ final class DataBrokerOperation: Operation { return filteredAndSortedOperationsData } - // swiftlint:disable:next function_body_length private func runOperation() async { let allBrokerProfileQueryData: [BrokerProfileQueryData] do { - allBrokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() + allBrokerProfileQueryData = try operationDependencies.database.fetchAllBrokerProfileQueryData() } catch { os_log("DataBrokerOperationsCollection error: runOperation, error: %{public}@", log: .error, error.localizedDescription) return @@ -178,31 +174,26 @@ final class DataBrokerOperation: Operation { try await DataBrokerProfileQueryOperationManager().runOperation(operationData: operationData, brokerProfileQueryData: brokerProfileData, - database: database, - notificationCenter: notificationCenter, - runner: runner, - pixelHandler: pixelHandler, + database: operationDependencies.database, + notificationCenter: operationDependencies.notificationCenter, + runner: operationDependencies.runnerProvider.getJobRunner(), + pixelHandler: operationDependencies.pixelHandler, showWebView: showWebView, - isManualScan: operationType == .manualScan, - userNotificationService: userNotificationService, + isImmediateOperation: operationType == .scan, + userNotificationService: operationDependencies.userNotificationService, shouldRunNextStep: { [weak self] in guard let self = self else { return false } return !self.isCancelled }) - if let sleepInterval = brokerTimeInterval { - os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) - try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) - } + let sleepInterval = operationDependencies.config.intervalBetweenSameBrokerOperations + os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) + try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) } catch { os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) - self.error = error - if let error = error as? DataBrokerProtectionError, - let dataBrokerName = brokerProfileQueriesData.first?.dataBroker.name { - pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) - } + errorDelegate?.dataBrokerOperationDidError(error, withBrokerName: brokerProfileQueriesData.first?.dataBroker.name) } } @@ -222,3 +213,4 @@ final class DataBrokerOperation: Operation { os_log("Finished operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) } } +// swiftlint:enable explicit_non_final_class diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index c1975440f3..72b5772a8b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -35,7 +35,7 @@ protocol OperationsManager { runner: WebJobRunner, pixelHandler: EventMapping, showWebView: Bool, - isManualScan: Bool, + isImmediateOperation: Bool, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws } @@ -58,7 +58,7 @@ extension OperationsManager { runner: runner, pixelHandler: pixelHandler, showWebView: false, - isManualScan: isManual, + isImmediateOperation: isManual, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } @@ -73,7 +73,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { runner: WebJobRunner, pixelHandler: EventMapping, showWebView: Bool = false, - isManualScan: Bool = false, + isImmediateOperation: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { @@ -84,7 +84,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { notificationCenter: notificationCenter, pixelHandler: pixelHandler, showWebView: showWebView, - isManual: isManualScan, + isManual: isImmediateOperation, userNotificationService: userNotificationService, shouldRunNextStep: shouldRunNextStep) } else if let optOutJobData = operationData as? OptOutJobData { @@ -126,7 +126,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) let stageCalculator = DataBrokerProtectionStageDurationCalculator(dataBroker: brokerProfileQueryData.dataBroker.name, handler: pixelHandler, - isManualScan: isManual) + isImmediateOperation: isManual) do { let event = HistoryEvent(brokerId: brokerId, profileQueryId: profileQueryId, type: .scanStarted) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index cc0df841f6..8e47a7b6a7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -98,7 +98,13 @@ final class AppVersionNumber: AppVersionNumberProvider { var versionNumber: String = AppVersion.shared.versionNumber } -public struct DataBrokerProtectionBrokerUpdater { +protocol DataBrokerProtectionBrokerUpdater { + static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? + func updateBrokers() + func checkForUpdatesInBrokerJSONFiles() +} + +public struct DefaultDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrokerUpdater { private let repository: BrokerUpdaterRepository private let resources: ResourcesRepository @@ -118,9 +124,9 @@ public struct DataBrokerProtectionBrokerUpdater { self.pixelHandler = pixelHandler } - public static func provide() -> DataBrokerProtectionBrokerUpdater? { + public static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? { if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { - return DataBrokerProtectionBrokerUpdater(vault: vault) + return DefaultDataBrokerProtectionBrokerUpdater(vault: vault) } os_log("Error when trying to create vault for data broker protection updater debug menu item", log: .dataBrokerProtection) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift similarity index 92% rename from LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift rename to LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift index fae7c89425..ae72a5ae18 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculatorUseCase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift @@ -1,5 +1,5 @@ // -// MismatchCalculatorUseCase.swift +// MismatchCalculator.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -36,7 +36,12 @@ enum MismatchValues: Int { } } -struct MismatchCalculatorUseCase { +protocol MismatchCalculator { + init(database: DataBrokerProtectionRepository, pixelHandler: EventMapping) + func calculateMismatches() +} + +struct DefaultMismatchCalculator: MismatchCalculator { let database: DataBrokerProtectionRepository let pixelHandler: EventMapping diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift index ad7f3cd61d..dd4f4c1c4f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionPixels.swift @@ -64,7 +64,7 @@ public enum DataBrokerProtectionPixels { static let wasOnWaitlist = "was_on_waitlist" static let httpCode = "http_code" static let backendServiceCallSite = "backend_service_callsite" - static let isManualScan = "is_manual_scan" + static let isImmediateOperation = "is_manual_scan" static let durationInMs = "duration_in_ms" static let profileQueries = "profile_queries" static let hasError = "has_error" @@ -101,8 +101,8 @@ public enum DataBrokerProtectionPixels { case backgroundAgentRunOperationsAndStartSchedulerIfPossible case backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile // There's currently no point firing this because the scheduler never calls the completion with an error - // case backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackError(error: Error) - case backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler + // case backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackError(error: Error) + case backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler // IPC server events case ipcServerStartSchedulerCalledByApp @@ -128,8 +128,8 @@ public enum DataBrokerProtectionPixels { case ipcServerOptOutAllBrokers case ipcServerOptOutAllBrokersCompletion(error: Error?) - case ipcServerRunQueuedOperations - case ipcServerRunQueuedOperationsCompletion(error: Error?) + case ipcServerStartScheduledOperations + case ipcServerStartScheduledOperationsCompletion(error: Error?) case ipcServerRunAllOperations // DataBrokerProtection User Notifications @@ -143,9 +143,9 @@ public enum DataBrokerProtectionPixels { case dataBrokerProtectionNotificationOpenedAllRecordsRemoved // Scan/Search pixels - case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int, isManualScan: Bool) - case scanFailed(dataBroker: String, duration: Double, tries: Int, isManualScan: Bool) - case scanError(dataBroker: String, duration: Double, category: String, details: String, isManualScan: Bool) + case scanSuccess(dataBroker: String, matchesFound: Int, duration: Double, tries: Int, isImmediateOperation: Bool) + case scanFailed(dataBroker: String, duration: Double, tries: Int, isImmediateOperation: Bool) + case scanError(dataBroker: String, duration: Double, category: String, details: String, isImmediateOperation: Bool) // KPIs - engagement case dailyActiveUser @@ -221,7 +221,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .backgroundAgentRunOperationsAndStartSchedulerIfPossible: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible" case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_no-saved-profile" - case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_callback_start-scheduler" + case .backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler: return "m_mac_dbp_background-agent-run-operations-and-start-scheduler-if-possible_callback_start-scheduler" case .ipcServerStartSchedulerCalledByApp: return "m_mac_dbp_ipc-server_start-scheduler_called-by-app" case .ipcServerStartSchedulerReceivedByAgent: return "m_mac_dbp_ipc-server_start-scheduler_received-by-agent" @@ -245,8 +245,8 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .ipcServerOptOutAllBrokers: return "m_mac_dbp_ipc-server_opt-out-all-brokers" case .ipcServerOptOutAllBrokersCompletion: return "m_mac_dbp_ipc-server_opt-out-all-brokers_completion" - case .ipcServerRunQueuedOperations: return "m_mac_dbp_ipc-server_run-queued-operations" - case .ipcServerRunQueuedOperationsCompletion: return "m_mac_dbp_ipc-server_run-queued-operations_completion" + case .ipcServerStartScheduledOperations: return "m_mac_dbp_ipc-server_run-queued-operations" + case .ipcServerStartScheduledOperationsCompletion: return "m_mac_dbp_ipc-server_run-queued-operations_completion" case .ipcServerRunAllOperations: return "m_mac_dbp_ipc-server_run-all-operations" // User Notifications @@ -373,7 +373,7 @@ extension DataBrokerProtectionPixels: PixelKitEvent { case .backgroundAgentStarted, .backgroundAgentRunOperationsAndStartSchedulerIfPossible, .backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile, - .backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler, + .backgroundAgentRunOperationsAndStartSchedulerIfPossibleStartScheduledOperationsCallbackStartScheduler, .backgroundAgentStartedStoppingDueToAnotherInstanceRunning, .dataBrokerProtectionNotificationSentFirstScanComplete, .dataBrokerProtectionNotificationOpenedFirstScanComplete, @@ -417,16 +417,16 @@ extension DataBrokerProtectionPixels: PixelKitEvent { .ipcServerScanAllBrokersCompletionCalledOnAppAfterInterruption, .ipcServerOptOutAllBrokers, .ipcServerOptOutAllBrokersCompletion, - .ipcServerRunQueuedOperations, - .ipcServerRunQueuedOperationsCompletion, + .ipcServerStartScheduledOperations, + .ipcServerStartScheduledOperationsCompletion, .ipcServerRunAllOperations: return [Consts.bundleIDParamKey: Bundle.main.bundleIdentifier ?? "nil"] - case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] - case .scanFailed(let dataBroker, let duration, let tries, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isManualScan: isManualScan.description] - case .scanError(let dataBroker, let duration, let category, let details, let isManualScan): - return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details, Consts.isManualScan: isManualScan.description] + case .scanSuccess(let dataBroker, let matchesFound, let duration, let tries, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.matchesFoundKey: String(matchesFound), Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isImmediateOperation: isImmediateOperation.description] + case .scanFailed(let dataBroker, let duration, let tries, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.triesKey: String(tries), Consts.isImmediateOperation: isImmediateOperation.description] + case .scanError(let dataBroker, let duration, let category, let details, let isImmediateOperation): + return [Consts.dataBrokerParamKey: dataBroker, Consts.durationParamKey: String(duration), Consts.errorCategoryKey: category, Consts.errorDetailsKey: details, Consts.isImmediateOperation: isImmediateOperation.description] case .generateEmailHTTPErrorDaily(let statusCode, let environment, let wasOnWaitlist): return [Consts.environmentKey: environment, Consts.httpCode: String(statusCode), @@ -470,7 +470,7 @@ public class DataBrokerProtectionPixelsHandler: EventMapping Double func durationSinceStartTime() -> Double @@ -63,7 +63,7 @@ protocol StageDurationCalculator { } final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator { - let isManualScan: Bool + let isImmediateOperation: Bool let handler: EventMapping let attemptId: UUID let dataBroker: String @@ -77,13 +77,13 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator startTime: Date = Date(), dataBroker: String, handler: EventMapping, - isManualScan: Bool = false) { + isImmediateOperation: Bool = false) { self.attemptId = attemptId self.startTime = startTime self.lastStateTime = startTime self.dataBroker = dataBroker self.handler = handler - self.isManualScan = isManualScan + self.isImmediateOperation = isImmediateOperation } /// Returned in milliseconds @@ -163,11 +163,11 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator } func fireScanSuccess(matchesFound: Int) { - handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) + handler.fire(.scanSuccess(dataBroker: dataBroker, matchesFound: matchesFound, duration: durationSinceStartTime(), tries: 1, isImmediateOperation: isImmediateOperation)) } func fireScanFailed() { - handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1, isManualScan: isManualScan)) + handler.fire(.scanFailed(dataBroker: dataBroker, duration: durationSinceStartTime(), tries: 1, isImmediateOperation: isImmediateOperation)) } func fireScanError(error: Error) { @@ -205,7 +205,7 @@ final class DataBrokerProtectionStageDurationCalculator: StageDurationCalculator duration: durationSinceStartTime(), category: errorCategory.toString, details: error.localizedDescription, - isManualScan: isManualScan + isImmediateOperation: isImmediateOperation ) ) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift index 41d02970af..aba746a849 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerOperationsCreator.swift @@ -23,6 +23,7 @@ protocol DataBrokerOperationsCreator { func operations(forOperationType operationType: OperationType, withPriorityDate priorityDate: Date?, showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, operationDependencies: DataBrokerOperationDependencies) throws -> [DataBrokerOperation] } @@ -31,6 +32,7 @@ final class DefaultDataBrokerOperationsCreator: DataBrokerOperationsCreator { func operations(forOperationType operationType: OperationType, withPriorityDate priorityDate: Date?, showWebView: Bool, + errorDelegate: DataBrokerOperationErrorDelegate, operationDependencies: DataBrokerOperationDependencies) throws -> [DataBrokerOperation] { let brokerProfileQueryData = try operationDependencies.database.fetchAllBrokerProfileQueryData() @@ -42,10 +44,11 @@ final class DefaultDataBrokerOperationsCreator: DataBrokerOperationsCreator { if !visitedDataBrokerIDs.contains(dataBrokerID) { let collection = DataBrokerOperation(dataBrokerID: dataBrokerID, - operationType: operationType, - priorityDate: priorityDate, - showWebView: showWebView, - operationDependencies: operationDependencies) + operationType: operationType, + priorityDate: priorityDate, + showWebView: showWebView, + errorDelegate: errorDelegate, + operationDependencies: operationDependencies) operations.append(collection) visitedDataBrokerIDs.insert(dataBrokerID) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift deleted file mode 100644 index 5cc6c42ea0..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessor.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// DataBrokerProtectionProcessor.swift -// -// Copyright © 2023 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common -import BrowserServicesKit - -final class DataBrokerProtectionProcessor { - private let database: DataBrokerProtectionRepository - private let config: DataBrokerProtectionProcessorConfiguration - private let jobRunnerProvider: JobRunnerProvider - private let notificationCenter: NotificationCenter - private let operationQueue: OperationQueue - private var pixelHandler: EventMapping - private let userNotificationService: DataBrokerProtectionUserNotificationService - private let engagementPixels: DataBrokerProtectionEngagementPixels - private let eventPixels: DataBrokerProtectionEventPixels - - init(database: DataBrokerProtectionRepository, - config: DataBrokerProtectionProcessorConfiguration = DataBrokerProtectionProcessorConfiguration(), - jobRunnerProvider: JobRunnerProvider, - notificationCenter: NotificationCenter = NotificationCenter.default, - pixelHandler: EventMapping, - userNotificationService: DataBrokerProtectionUserNotificationService) { - - self.database = database - self.config = config - self.jobRunnerProvider = jobRunnerProvider - self.notificationCenter = notificationCenter - self.operationQueue = OperationQueue() - self.pixelHandler = pixelHandler - self.userNotificationService = userNotificationService - self.engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) - self.eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) - } - - // MARK: - Public functions - func startManualScans(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { - - operationQueue.cancelAllOperations() - runOperations(operationType: .manualScan, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Scans done", log: .dataBrokerProtection) - completion?(errors) - self.calculateMisMatches() - } - } - - private func calculateMisMatches() { - let mismatchUseCase = MismatchCalculatorUseCase(database: database, pixelHandler: pixelHandler) - mismatchUseCase.calculateMismatches() - } - - func runAllOptOutOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { - operationQueue.cancelAllOperations() - runOperations(operationType: .optOut, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Optouts done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func runQueuedOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil ) { - runOperations(operationType: .all, - priorityDate: Date(), - showWebView: showWebView) { errors in - os_log("Queued operations done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func runAllOperations(showWebView: Bool = false, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil ) { - runOperations(operationType: .all, - priorityDate: nil, - showWebView: showWebView) { errors in - os_log("Queued operations done", log: .dataBrokerProtection) - completion?(errors) - } - } - - func stopAllOperations() { - operationQueue.cancelAllOperations() - } - - // MARK: - Private functions - private func runOperations(operationType: OperationType, - priorityDate: Date?, - showWebView: Bool, - completion: @escaping ((DataBrokerProtectionAgentErrorCollection?) -> Void)) { - - self.operationQueue.maxConcurrentOperationCount = config.concurrentOperationsFor(operationType) - // Before running new operations we check if there is any updates to the broker files. - if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: DataBrokerProtectionSecureVaultErrorReporter.shared) { - let brokerUpdater = DataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) - brokerUpdater.checkForUpdatesInBrokerJSONFiles() - } - - // This will fire the DAU/WAU/MAU pixels, - engagementPixels.fireEngagementPixel() - // This will try to fire the event weekly report pixels - eventPixels.tryToFireWeeklyPixels() - - let operations: [DataBrokerOperation] - - do { - // Note: The next task in this project will inject the dependencies & builder into our new 'QueueManager' type - - let dependencies = DefaultDataBrokerOperationDependencies(database: database, - brokerTimeInterval: config.intervalBetweenSameBrokerOperations, - runnerProvider: jobRunnerProvider, - notificationCenter: notificationCenter, - pixelHandler: pixelHandler, - userNotificationService: userNotificationService) - - operations = try DefaultDataBrokerOperationsCreator().operations(forOperationType: operationType, - withPriorityDate: priorityDate, - showWebView: showWebView, - operationDependencies: dependencies) - - for operation in operations { - operationQueue.addOperation(operation) - } - } catch { - os_log("DataBrokerProtectionProcessor error: runOperations, error: %{public}@", log: .error, error.localizedDescription) - operationQueue.addBarrierBlock { - completion(DataBrokerProtectionAgentErrorCollection(oneTimeError: error)) - } - return - } - - operationQueue.addBarrierBlock { - let operationErrors = operations.compactMap { $0.error } - let errorCollection = operationErrors.count != 0 ? DataBrokerProtectionAgentErrorCollection(operationErrors: operationErrors) : nil - completion(errorCollection) - } - } - - deinit { - os_log("Deinit DataBrokerProtectionProcessor", log: .dataBrokerProtection) - } -} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift index 3281b66c37..2914b43f5e 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionProcessorConfiguration.swift @@ -29,7 +29,7 @@ struct DataBrokerProtectionProcessorConfiguration { switch operation { case .all, .optOut: return concurrentOperationsDifferentBrokers - case .manualScan: + case .scan: return concurrentOperationsOnManualScans } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift new file mode 100644 index 0000000000..ab21bd6d35 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -0,0 +1,262 @@ +// +// DataBrokerProtectionQueueManager.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 Common +import Foundation + +protocol DataBrokerProtectionOperationQueue { + var maxConcurrentOperationCount: Int { get set } + func cancelAllOperations() + func addOperation(_ op: Operation) + func addBarrierBlock(_ barrier: @escaping @Sendable () -> Void) +} + +extension OperationQueue: DataBrokerProtectionOperationQueue {} + +enum DataBrokerProtectionQueueMode { + case idle + case immediate(completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + case scheduled(completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + + var priorityDate: Date? { + switch self { + case .idle, .immediate: + return nil + case .scheduled: + return Date() + } + } + + func canBeInterruptedBy(newMode: DataBrokerProtectionQueueMode) -> Bool { + switch (self, newMode) { + case (.idle, _): + return true + case (_, .immediate): + return true + default: + return false + } + } +} + +enum DataBrokerProtectionQueueError: Error { + case cannotInterrupt +} + +enum DataBrokerProtectionQueueManagerDebugCommand { + case startOptOutOperations(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) +} + +protocol DataBrokerProtectionQueueManager { + + init(operationQueue: DataBrokerProtectionOperationQueue, + operationsCreator: DataBrokerOperationsCreator, + mismatchCalculator: MismatchCalculator, + brokerUpdater: DataBrokerProtectionBrokerUpdater?, + pixelHandler: EventMapping) + + func startImmediateOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + func startScheduledOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) + + func stopAllOperations() + + func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) +} + +final class DefaultDataBrokerProtectionQueueManager: DataBrokerProtectionQueueManager { + + private var operationQueue: DataBrokerProtectionOperationQueue + private let operationsCreator: DataBrokerOperationsCreator + private let mismatchCalculator: MismatchCalculator + private let brokerUpdater: DataBrokerProtectionBrokerUpdater? + private let pixelHandler: EventMapping + + private var mode = DataBrokerProtectionQueueMode.idle + private var operationErrors: [Error] = [] + + init(operationQueue: DataBrokerProtectionOperationQueue, + operationsCreator: DataBrokerOperationsCreator, + mismatchCalculator: MismatchCalculator, + brokerUpdater: DataBrokerProtectionBrokerUpdater?, + pixelHandler: EventMapping) { + + self.operationQueue = operationQueue + self.operationsCreator = operationsCreator + self.mismatchCalculator = mismatchCalculator + self.brokerUpdater = brokerUpdater + self.pixelHandler = pixelHandler + } + + func startImmediateOperationsIfPermitted(showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + let newMode = DataBrokerProtectionQueueMode.immediate(completion: completion) + startOperationsIfPermitted(forNewMode: newMode, + type: .scan, + showWebView: showWebView, + operationDependencies: operationDependencies) { [weak self] errors in + completion?(errors) + self?.mismatchCalculator.calculateMismatches() + } + } + + 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 stopAllOperations() { + cancelCurrentModeAndResetIfNeeded() + } + + func execute(_ command: DataBrokerProtectionQueueManagerDebugCommand) { + guard case .startOptOutOperations(let showWebView, + let operationDependencies, + let completion) = command else { return } + + addOperations(withType: .optOut, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } +} + +private extension DefaultDataBrokerProtectionQueueManager { + + func startOperationsIfPermitted(forNewMode newMode: DataBrokerProtectionQueueMode, + type: OperationType, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + guard mode.canBeInterruptedBy(newMode: newMode) else { + let error = DataBrokerProtectionQueueError.cannotInterrupt + let errorCollection = DataBrokerProtectionAgentErrorCollection(oneTimeError: error) + completion?(errorCollection) + return + } + + cancelCurrentModeAndResetIfNeeded() + + mode = newMode + + updateBrokerData() + + firePixels(operationDependencies: operationDependencies) + + addOperations(withType: type, + priorityDate: mode.priorityDate, + showWebView: showWebView, + operationDependencies: operationDependencies, + completion: completion) + } + + func cancelCurrentModeAndResetIfNeeded() { + switch mode { + case .immediate(let completion), .scheduled(let completion): + operationQueue.cancelAllOperations() + completion?(errorCollectionForCurrentOperations()) + resetModeAndClearErrors() + default: + break + } + } + + func resetModeAndClearErrors() { + mode = .idle + operationErrors = [] + } + + func updateBrokerData() { + // Update broker files if applicable + brokerUpdater?.checkForUpdatesInBrokerJSONFiles() + } + + func addOperations(withType type: OperationType, + priorityDate: Date? = nil, + showWebView: Bool, + operationDependencies: DataBrokerOperationDependencies, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)?) { + + operationQueue.maxConcurrentOperationCount = operationDependencies.config.concurrentOperationsFor(type) + + // Use builder to build operations + let operations: [DataBrokerOperation] + do { + operations = try operationsCreator.operations(forOperationType: type, + withPriorityDate: priorityDate, + showWebView: showWebView, + errorDelegate: self, + operationDependencies: operationDependencies) + + for collection in operations { + operationQueue.addOperation(collection) + } + } catch { + os_log("DataBrokerProtectionProcessor error: addOperations, error: %{public}@", log: .error, error.localizedDescription) + completion?(DataBrokerProtectionAgentErrorCollection(oneTimeError: error)) + return + } + + operationQueue.addBarrierBlock { [weak self] in + let errorCollection = self?.errorCollectionForCurrentOperations() + completion?(errorCollection) + self?.resetModeAndClearErrors() + } + } + + func errorCollectionForCurrentOperations() -> DataBrokerProtectionAgentErrorCollection? { + return operationErrors.count != 0 ? DataBrokerProtectionAgentErrorCollection(operationErrors: operationErrors) : nil + } + + func firePixels(operationDependencies: DataBrokerOperationDependencies) { + let database = operationDependencies.database + let pixelHandler = operationDependencies.pixelHandler + + let engagementPixels = DataBrokerProtectionEngagementPixels(database: database, handler: pixelHandler) + let eventPixels = DataBrokerProtectionEventPixels(database: database, handler: pixelHandler) + + // This will fire the DAU/WAU/MAU pixels, + engagementPixels.fireEngagementPixel() + // This will try to fire the event weekly report pixels + eventPixels.tryToFireWeeklyPixels() + } +} + +extension DefaultDataBrokerProtectionQueueManager: DataBrokerOperationErrorDelegate { + func dataBrokerOperationDidError(_ error: any Error, withBrokerName brokerName: String?) { + operationErrors.append(error) + + if let error = error as? DataBrokerProtectionError, let dataBrokerName = brokerName { + pixelHandler.fire(.error(error: error, dataBroker: dataBrokerName)) + } + } +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift index d9ac509201..aa8fc9e004 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionScheduler.swift @@ -66,18 +66,35 @@ public final class DefaultDataBrokerProtectionScheduler { public var lastSchedulerSessionStartTimestamp: Date? - private lazy var dataBrokerProcessor: DataBrokerProtectionProcessor = { - + private lazy var queueManager: DataBrokerProtectionQueueManager = { + let operationQueue = OperationQueue() + let operationsBuilder = DefaultDataBrokerOperationsCreator() + let mismatchCalculator = DefaultMismatchCalculator(database: dataManager.database, + pixelHandler: pixelHandler) + + var brokerUpdater: DataBrokerProtectionBrokerUpdater? + if let vault = try? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: nil) { + brokerUpdater = DefaultDataBrokerProtectionBrokerUpdater(vault: vault, pixelHandler: pixelHandler) + } + + return DefaultDataBrokerProtectionQueueManager(operationQueue: operationQueue, + operationsCreator: operationsBuilder, + mismatchCalculator: mismatchCalculator, + brokerUpdater: brokerUpdater, + pixelHandler: pixelHandler) + }() + + private lazy var operationDependencies: DataBrokerOperationDependencies = { let runnerProvider = DataBrokerJobRunnerProvider(privacyConfigManager: privacyConfigManager, - contentScopeProperties: contentScopeProperties, - emailService: emailService, - captchaService: captchaService) - - return DataBrokerProtectionProcessor(database: dataManager.database, - jobRunnerProvider: runnerProvider, - notificationCenter: notificationCenter, - pixelHandler: pixelHandler, - userNotificationService: userNotificationService) + contentScopeProperties: contentScopeProperties, + emailService: emailService, + captchaService: captchaService) + + return DefaultDataBrokerOperationDependencies(database: dataManager.database, + config: DataBrokerProtectionProcessorConfiguration(), + runnerProvider: runnerProvider, + notificationCenter: notificationCenter, + pixelHandler: pixelHandler, userNotificationService: userNotificationService) }() public init(privacyConfigManager: PrivacyConfigurationManaging, @@ -128,15 +145,15 @@ public final class DefaultDataBrokerProtectionScheduler { self.status = .running os_log("Scheduler running...", log: .dataBrokerProtection) self.currentOperation = .queued - self.dataBrokerProcessor.runQueuedOperations(showWebView: showWebView) { [weak self] errors in + self.queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, operationDependencies: self.operationDependencies) { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { - os_log("Error during startScheduler in dataBrokerProcessor.runQueuedOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Error during startScheduler in dataBrokerProcessor.startScheduledOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startScheduler")) } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during startScheduler in dataBrokerProcessor.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during startScheduler in dataBrokerProcessor.startScheduledOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } self?.status = .idle @@ -150,33 +167,10 @@ public final class DefaultDataBrokerProtectionScheduler { os_log("Stopping scheduler...", log: .dataBrokerProtection) activity.invalidate() status = .stopped - dataBrokerProcessor.stopAllOperations() - } - - public func runAllOperations(showWebView: Bool = false) { - guard self.currentOperation != .manualScan else { - os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) - return - } - - os_log("Running all operations...", log: .dataBrokerProtection) - self.currentOperation = .all - self.dataBrokerProcessor.runAllOperations(showWebView: showWebView) { [weak self] errors in - if let errors = errors { - if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.runAllOperations")) - } - if let operationErrors = errors.operationErrors, - operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runAllOperations in dataBrokerProcessor.runAllOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) - } - } - self?.currentOperation = .idle - } + queueManager.stopAllOperations() } - public func runQueuedOperations(showWebView: Bool = false, + public func startScheduledOperations(showWebView: Bool = false, completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { guard self.currentOperation != .manualScan else { os_log("Manual scan in progress, returning...", log: .dataBrokerProtection) @@ -185,16 +179,17 @@ public final class DefaultDataBrokerProtectionScheduler { os_log("Running queued operations...", log: .dataBrokerProtection) self.currentOperation = .queued - dataBrokerProcessor.runQueuedOperations(showWebView: showWebView, - completion: { [weak self] errors in + queueManager.startScheduledOperationsIfPermitted(showWebView: showWebView, + operationDependencies: operationDependencies, + completion: { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { - os_log("Error during DefaultDataBrokerProtectionScheduler.runQueuedOperations in dataBrokerProcessor.runQueuedOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.runQueuedOperations")) + os_log("Error during DefaultDataBrokerProtectionScheduler.startScheduledOperations in dataBrokerProcessor.startScheduledOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startScheduledOperations")) } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.runQueuedOperations in dataBrokerProcessor.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startScheduledOperations in dataBrokerProcessor.startScheduledOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } completion?(errors) @@ -203,9 +198,9 @@ public final class DefaultDataBrokerProtectionScheduler { } - public func startManualScan(showWebView: Bool = false, - startTime: Date, - completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { + public func startImmediateOperations(showWebView: Bool = false, + startTime: Date, + completion: ((DataBrokerProtectionAgentErrorCollection?) -> Void)? = nil) { pixelHandler.fire(.initialScanPreStartDuration(duration: (Date().timeIntervalSince(startTime) * 1000).rounded(.towardZero))) let backgroundAgentManualScanStartTime = Date() stopScheduler() @@ -213,7 +208,8 @@ public final class DefaultDataBrokerProtectionScheduler { userNotificationService.requestNotificationPermission() self.currentOperation = .manualScan os_log("Scanning all brokers...", log: .dataBrokerProtection) - dataBrokerProcessor.startManualScans(showWebView: showWebView) { [weak self] errors in + queueManager.startImmediateOperationsIfPermitted(showWebView: showWebView, + operationDependencies: operationDependencies) { [weak self] errors in guard let self = self else { return } self.startScheduler(showWebView: showWebView) @@ -231,15 +227,15 @@ public final class DefaultDataBrokerProtectionScheduler { if let oneTimeError = errors.oneTimeError { switch oneTimeError { case DataBrokerProtectionAgentInterfaceError.operationsInterrupted: - os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + os_log("Interrupted during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) default: - os_log("Error during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) - self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startManualScan")) + os_log("Error during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + self.pixelHandler.fire(.generalError(error: oneTimeError, functionOccurredIn: "DefaultDataBrokerProtectionScheduler.startImmediateOperations")) } } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startManualScan in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + os_log("Operation error(s) during DefaultDataBrokerProtectionScheduler.startImmediateOperations in dataBrokerProcessor.runAllScanOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) } } self.currentOperation = .idle @@ -269,8 +265,7 @@ public final class DefaultDataBrokerProtectionScheduler { os_log("Opting out all brokers...", log: .dataBrokerProtection) self.currentOperation = .optOutAll - self.dataBrokerProcessor.runAllOptOutOperations(showWebView: showWebView, - completion: { [weak self] errors in + queueManager.execute(.startOptOutOperations(showWebView: showWebView, operationDependencies: operationDependencies) { [weak self] errors in if let errors = errors { if let oneTimeError = errors.oneTimeError { os_log("Error during DefaultDataBrokerProtectionScheduler.optOutAllBrokers in dataBrokerProcessor.runAllOptOutOperations(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift index 3caeee6475..42060fdb9b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationsCreatorTests.swift @@ -38,7 +38,7 @@ final class DataBrokerOperationsCreatorTests: XCTestCase { mockUserNotification = MockUserNotification() mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, - brokerTimeInterval: mockSchedulerConfig.intervalBetweenSameBrokerOperations, + config: mockSchedulerConfig, runnerProvider: mockRunnerProvider, notificationCenter: .default, pixelHandler: mockPixelHandler, @@ -70,9 +70,10 @@ final class DataBrokerOperationsCreatorTests: XCTestCase { mockDatabase.brokerProfileQueryDataToReturn = dataBrokerProfileQueries // When - let result = try! sut.operations(forOperationType: .manualScan, + let result = try! sut.operations(forOperationType: .scan, withPriorityDate: Date(), showWebView: false, + errorDelegate: MockDataBrokerOperationErrorDelegate(), operationDependencies: mockDependencies) // Then diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift index fe8eec11c1..f8e2be4be2 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionProcessorConfigurationTests.swift @@ -25,7 +25,7 @@ final class DataBrokerProtectionProcessorConfigurationTests: XCTestCase { private let sut = DataBrokerProtectionProcessorConfiguration() func testWhenOperationIsManualScans_thenConcurrentOperationsBetweenBrokersIsSix() { - let value = sut.concurrentOperationsFor(.manualScan) + let value = sut.concurrentOperationsFor(.scan) let expectedValue = 6 XCTAssertEqual(value, expectedValue) } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift new file mode 100644 index 0000000000..eb11ce957c --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueManagerTests.swift @@ -0,0 +1,350 @@ +// +// DataBrokerProtectionQueueManagerTests.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 XCTest +@testable import DataBrokerProtection + +final class DataBrokerProtectionQueueManagerTests: XCTestCase { + + private var sut: DefaultDataBrokerProtectionQueueManager! + + private var mockQueue: MockDataBrokerProtectionOperationQueue! + private var mockOperationsCreator: MockDataBrokerOperationsCreator! + private var mockDatabase: MockDatabase! + private var mockPixelHandler: MockPixelHandler! + private var mockMismatchCalculator: MockMismatchCalculator! + private var mockUpdater: MockDataBrokerProtectionBrokerUpdater! + private var mockSchedulerConfig = DataBrokerProtectionProcessorConfiguration() + private var mockRunnerProvider: MockRunnerProvider! + private var mockUserNotification: MockUserNotification! + private var mockOperationErrorDelegate: MockDataBrokerOperationErrorDelegate! + private var mockDependencies: DefaultDataBrokerOperationDependencies! + + override func setUpWithError() throws { + mockQueue = MockDataBrokerProtectionOperationQueue() + mockOperationsCreator = MockDataBrokerOperationsCreator() + mockDatabase = MockDatabase() + mockPixelHandler = MockPixelHandler() + mockMismatchCalculator = MockMismatchCalculator(database: mockDatabase, pixelHandler: mockPixelHandler) + mockUpdater = MockDataBrokerProtectionBrokerUpdater() + mockRunnerProvider = MockRunnerProvider() + mockUserNotification = MockUserNotification() + + mockDependencies = DefaultDataBrokerOperationDependencies(database: mockDatabase, + config: DataBrokerProtectionProcessorConfiguration(), + runnerProvider: mockRunnerProvider, + notificationCenter: .default, + pixelHandler: mockPixelHandler, + userNotificationService: mockUserNotification) + } + + func testWhenStartImmediateScan_andScanCompletesWithErrors_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) + mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] + let expectation = expectation(description: "Expected errors to be returned in completion") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.scan) + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 5) + XCTAssert(errorCollection.operationErrors?.count == 1) + XCTAssertNil(mockOperationsCreator.priorityDate) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } + + func testWhenStartScheduledScan_andScanCompletesWithErrors_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) + mockOperationsCreator.operationCollections = [mockOperation, mockOperationWithError] + let expectation = expectation(description: "Expected errors to be returned in completion") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.all) + + // When + sut.startScheduledOperationsIfPermitted(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 testWhenStartImmediateScan_andCurrentModeIsScheduled_thenCurrentOperationsAreInterrupted_andCurrentCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + 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) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // Given + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(errorCollection.operationErrors?.count == 2) + XCTAssert(mockQueue.didCallCancelCount == 1) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 4) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count >= 2) + } + + func testWhenStartImmediateScan_andCurrentModeIsImmediate_thenCurrentOperationsAreInterrupted_andCurrentCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + 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) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollection = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // Given + mockOperations = (5...8).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(errorCollection.operationErrors?.count == 2) + XCTAssert(mockQueue.didCallCancelCount == 1) + XCTAssert(mockQueue.operations.filter { !$0.isCancelled }.count == 4) + XCTAssert(mockQueue.operations.filter { $0.isCancelled }.count >= 2) + } + + func testWhenSecondImmedateScanInterruptsFirst_andFirstHadErrors_thenSecondCompletesOnlyWithNewErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + 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) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + var errorCollectionFirst: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollectionFirst = errors + } + + mockQueue.completeOperationsUpTo(index: 2) + + // Then + XCTAssert(mockQueue.operationCount == 2) + + // 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) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { errors in + errorCollectionSecond = errors + } + + mockQueue.completeAllOperations() + + // Then + XCTAssert(errorCollectionFirst.operationErrors?.count == 2) + XCTAssert(errorCollectionSecond.operationErrors?.count == 2) + XCTAssert(mockQueue.didCallCancelCount == 1) + } + + func testWhenStartScheduledScan_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 mockOperationsWithError = (6...10).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, operationDependencies: mockDependencies) { _ in } + + // Then + XCTAssert(mockQueue.operationCount == 10) + + // Given + mockOperations = (11...15).map { MockDataBrokerOperation(id: $0, operationType: .scan, errorDelegate: sut) } + mockOperationsWithError = (16...20).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut, + shouldError: true) } + mockOperationsCreator.operationCollections = mockOperations + mockOperationsWithError + let expectedError = DataBrokerProtectionQueueError.cannotInterrupt + var completionCalled = false + + // When + sut.startScheduledOperationsIfPermitted(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 testWhenOperationBuildingFails_thenCompletionIsCalledOnOperationCreationOneTimeError() async throws { + // Given + mockOperationsCreator.shouldError = true + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let expectation = expectation(description: "Expected completion to be called") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + // Then + await fulfillment(of: [expectation], timeout: 3) + XCTAssertNotNil(errorCollection.oneTimeError) + } + + func testWhenOperationsAreRunning_andStopAllIsCalled_thenAllAreCancelled_andCompletionIsCalledWithErrors() async throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let mockOperationsWithError = (1...2).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut, + shouldError: true) } + let mockOperations = (3...4).map { MockDataBrokerOperation(id: $0, + operationType: .scan, + errorDelegate: sut) } + mockOperationsCreator.operationCollections = mockOperationsWithError + mockOperations + let expectation = expectation(description: "Expected completion to be called") + var errorCollection: DataBrokerProtectionAgentErrorCollection! + + // When + sut.startImmediateOperationsIfPermitted(showWebView: false, + operationDependencies: mockDependencies) { errors in + errorCollection = errors + expectation.fulfill() + } + + mockQueue.completeOperationsUpTo(index: 2) + + sut.stopAllOperations() + + // Then + await fulfillment(of: [expectation], timeout: 3) + XCTAssert(errorCollection.operationErrors?.count == 2) + } + + func testWhenCallDebugOptOutCommand_thenOptOutOperationsAreCreated() throws { + // Given + sut = DefaultDataBrokerProtectionQueueManager(operationQueue: mockQueue, + operationsCreator: mockOperationsCreator, + mismatchCalculator: mockMismatchCalculator, + brokerUpdater: mockUpdater, + pixelHandler: mockPixelHandler) + let expectedConcurrentOperations = DataBrokerProtectionProcessorConfiguration().concurrentOperationsFor(.optOut) + XCTAssert(mockOperationsCreator.createdType == .scan) + + // When + sut.execute(.startOptOutOperations(showWebView: false, + operationDependencies: mockDependencies, + completion: nil)) + + // Then + XCTAssert(mockOperationsCreator.createdType == .optOut) + XCTAssertEqual(mockQueue.maxConcurrentOperationCount, expectedConcurrentOperations) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift new file mode 100644 index 0000000000..ef2ab05b38 --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionQueueModeTests.swift @@ -0,0 +1,122 @@ +// +// DataBrokerProtectionQueueModeTests.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. +// + +@testable import DataBrokerProtection +import XCTest + +final class DataBrokerProtectionQueueModeTests: XCTestCase { + + func testCurrentModeIdle_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeIdle_andNewModeScheduled_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeImmediate_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: { _ in })) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeImmediate_andNewModeScheduled_thenInterruptionNotAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertFalse(result) + } + + func testCurrentModeScheduled_andNewModeImmediate_thenInterruptionAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .immediate(completion: nil)) + + // Then + XCTAssertTrue(result) + } + + func testCurrentModeScheduled_andNewModeScheduled_thenInterruptionNotAllowed() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.canBeInterruptedBy(newMode: .scheduled(completion: nil)) + + // Then + XCTAssertFalse(result) + } + + func testWhenModeIsIdle_thenPriorityDateIsNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.idle + + // When + let result = sut.priorityDate + + // Then + XCTAssertNil(result) + } + + func testWhenModeIsImmediate_thenPriorityDateIsNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.immediate(completion: nil) + + // When + let result = sut.priorityDate + + // Then + XCTAssertNil(result) + } + + func testWhenModeIsScheduled_thenPriorityDateIsNotNil() throws { + // Given + let sut = DataBrokerProtectionQueueMode.scheduled(completion: nil) + + // When + let result = sut.priorityDate + + // Then + XCTAssertNotNil(result) + } +} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift index 2036ac9a1d..4f70f8934a 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionUpdaterTests.swift @@ -39,7 +39,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenNoVersionIsStored_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil sut.checkForUpdatesInBrokerJSONFiles() @@ -53,7 +53,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndPatchIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.1")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.1")) repository.lastCheckedVersion = "1.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -67,7 +67,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndMinorIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "1.73.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -81,7 +81,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndMajorIsLessThanCurrentOne_thenWeTryToUpdateBrokers() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "0.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -95,7 +95,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenVersionIsStoredAndIsEqualOrGreaterThanCurrentOne_thenCheckingUpdatesIsSkipped() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault, appVersion: MockAppVersion(versionNumber: "1.74.0")) repository.lastCheckedVersion = "1.74.0" sut.checkForUpdatesInBrokerJSONFiles() @@ -109,7 +109,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenSavedBrokerIsOnAnOldVersion_thenWeUpdateIt() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnOldVersionBroker = true @@ -127,7 +127,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenSavedBrokerIsOnTheCurrentVersion_thenWeDoNotUpdateIt() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.1", schedulingConfig: .mock)] vault.shouldReturnNewVersionBroker = true @@ -144,7 +144,7 @@ final class DataBrokerProtectionUpdaterTests: XCTestCase { func testWhenFileBrokerIsNotStored_thenWeAddTheBrokerAndScanOperations() { if let vault = self.vault { - let sut = DataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) + let sut = DefaultDataBrokerProtectionBrokerUpdater(repository: repository, resources: resources, vault: vault) repository.lastCheckedVersion = nil resources.brokersList = [.init(id: 1, name: "Broker", url: "broker.com", steps: [Step](), version: "1.0.0", schedulingConfig: .mock)] vault.profileQueries = [.mock] diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift index 396034791a..fb2af7862b 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/MismatchCalculatorUseCaseTests.swift @@ -40,7 +40,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -65,7 +65,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -90,7 +90,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -115,7 +115,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { .mockParentWith(historyEvents: parentHistoryEvents), .mockChildtWith(historyEvents: childHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) @@ -136,7 +136,7 @@ final class MismatchCalculatorUseCaseTests: XCTestCase { database.brokerProfileQueryDataToReturn = [ .mockParentWith(historyEvents: parentHistoryEvents) ] - let sut = MismatchCalculatorUseCase( + let sut = DefaultMismatchCalculator( database: database, pixelHandler: pixelHandler ) diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift index 5e71a5b755..a80b15eb19 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/Mocks.swift @@ -868,7 +868,7 @@ final class MockAppVersion: AppVersionNumberProvider { } final class MockStageDurationCalculator: StageDurationCalculator { - var isManualScan: Bool = false + var isImmediateOperation: Bool = false var attemptId: UUID = UUID() var stage: Stage? @@ -962,7 +962,6 @@ final class MockDataBrokerProtectionBackendServicePixels: DataBrokerProtectionBa } final class MockRunnerProvider: JobRunnerProvider { - func getJobRunner() -> any WebJobRunner { MockWebJobRunner() } @@ -1026,3 +1025,185 @@ extension DataBroker { ) } } + +final class MockDataBrokerProtectionOperationQueue: DataBrokerProtectionOperationQueue { + var maxConcurrentOperationCount = 1 + + var operations: [Operation] = [] + var operationCount: Int { + operations.count + } + + private(set) var didCallCancelCount = 0 + private(set) var didCallAddCount = 0 + private(set) var didCallAddBarrierBlockCount = 0 + + private var barrierBlock: (@Sendable () -> Void)? + + func cancelAllOperations() { + didCallCancelCount += 1 + self.operations.forEach { $0.cancel() } + } + + func addOperation(_ op: Operation) { + didCallAddCount += 1 + self.operations.append(op) + } + + func addBarrierBlock(_ barrier: @escaping @Sendable () -> Void) { + didCallAddBarrierBlockCount += 1 + self.barrierBlock = barrier + } + + func completeAllOperations() { + operations.forEach { $0.start() } + operations.removeAll() + barrierBlock?() + } + + func completeOperationsUpTo(index: Int) { + guard index < operationCount else { return } + + (0.. [DataBrokerOperation] { + guard !shouldError else { throw DataBrokerProtectionError.unknown("")} + self.createdType = operationType + self.priorityDate = priorityDate + return operationCollections + } +} + +final class MockMismatchCalculator: MismatchCalculator { + + private(set) var didCallCalculateMismatches = false + + init(database: any DataBrokerProtectionRepository, pixelHandler: Common.EventMapping) { } + + func calculateMismatches() { + didCallCalculateMismatches = true + } +} + +final class MockDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrokerUpdater { + + private(set) var didCallUpdateBrokers = false + private(set) var didCallCheckForUpdates = false + + static func provideForDebug() -> DefaultDataBrokerProtectionBrokerUpdater? { + nil + } + + func updateBrokers() { + didCallUpdateBrokers = true + } + + func checkForUpdatesInBrokerJSONFiles() { + didCallCheckForUpdates = true + } +}