From 820203512819e81c4dd5ff0b53711d0010a8253c Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 27 Nov 2023 14:26:12 +0100 Subject: [PATCH 01/39] Ensure environment & location setting always used (#576) --- .../PacketTunnelProvider.swift | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index c18f90a46..937977f73 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -645,7 +645,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } do { - try await updateTunnelConfiguration(serverSelectionMethod: serverSelectionMethod) + try await updateTunnelConfiguration(environment: settings.selectedEnvironment, serverSelectionMethod: serverSelectionMethod) } catch { return } @@ -655,21 +655,12 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { // MARK: - Tunnel Configuration @MainActor - public func updateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment = .default, reassert: Bool = true) async throws { - let serverSelectionMethod: NetworkProtectionServerSelectionMethod - - switch settings.selectedServer { - case .automatic: - serverSelectionMethod = .automatic - case .endpoint(let serverName): - serverSelectionMethod = .preferredServer(serverName: serverName) - } - - try await updateTunnelConfiguration(environment: environment, serverSelectionMethod: serverSelectionMethod, reassert: reassert) + public func updateTunnelConfiguration(reassert: Bool = true) async throws { + try await updateTunnelConfiguration(environment: settings.selectedEnvironment, serverSelectionMethod: currentServerSelectionMethod, reassert: reassert) } @MainActor - public func updateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment = .default, serverSelectionMethod: NetworkProtectionServerSelectionMethod, reassert: Bool = true) async throws { + public func updateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment, serverSelectionMethod: NetworkProtectionServerSelectionMethod, reassert: Bool = true) async throws { let tunnelConfiguration = try await generateTunnelConfiguration(environment: environment, serverSelectionMethod: serverSelectionMethod, @@ -705,7 +696,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } @MainActor - private func generateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment = .default, serverSelectionMethod: NetworkProtectionServerSelectionMethod, includedRoutes: [IPAddressRange], excludedRoutes: [IPAddressRange]) async throws -> TunnelConfiguration { + private func generateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment, serverSelectionMethod: NetworkProtectionServerSelectionMethod, includedRoutes: [IPAddressRange], excludedRoutes: [IPAddressRange]) async throws -> TunnelConfiguration { let configurationResult: (TunnelConfiguration, NetworkProtectionServerInfo) @@ -827,7 +818,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { Task { if case .connected = connectionStatus { - try? await updateTunnelConfiguration(serverSelectionMethod: serverSelectionMethod) + try? await updateTunnelConfiguration(environment: settings.selectedEnvironment, serverSelectionMethod: serverSelectionMethod) } completionHandler?(nil) } @@ -902,7 +893,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { settings.selectedServer = .automatic if case .connected = connectionStatus { - try? await updateTunnelConfiguration(serverSelectionMethod: .automatic) + try? await updateTunnelConfiguration() } } completionHandler?(nil) @@ -916,7 +907,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { settings.selectedServer = .endpoint(serverName) if case .connected = connectionStatus { - try? await updateTunnelConfiguration(serverSelectionMethod: .preferredServer(serverName: serverName)) + try? await updateTunnelConfiguration(environment: settings.selectedEnvironment, serverSelectionMethod: .preferredServer(serverName: serverName)) } completionHandler?(nil) } From 543e1d7ed9b5743d4e6b0ebe18a5fbf8f1441f02 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Mon, 27 Nov 2023 17:25:01 +0100 Subject: [PATCH 02/39] Add proper migration tests (#577) Task/Issue URL: https://app.asana.com/0/414709148257752/1206032628720063/f https://app.asana.com/0/414709148257752/1206020815014024/f iOS PR: duckduckgo/iOS#2196 macOS PR: duckduckgo/macos-browser#1889 What kind of version bump will this require?: Patch Description: Fix possible crash when migrating from older DB versions. --- Package.swift | 18 + ...BookmarkFormFactorFavoritesMigration.swift | 37 +- .../BookmarksTestDBBuilder.swift | 326 ++++++++++++++++++ .../BookmarkMigrationTests.swift | 189 ++++++++++ Tests/BookmarksTests/BookmarkUtilsTests.swift | 51 --- .../Resources/Bookmarks_V1.sqlite | Bin 0 -> 32768 bytes .../Resources/Bookmarks_V1.sqlite-shm | Bin 0 -> 32768 bytes .../Resources/Bookmarks_V1.sqlite-wal | Bin 0 -> 16512 bytes .../Resources/Bookmarks_V2.sqlite | Bin 0 -> 49152 bytes .../Resources/Bookmarks_V2.sqlite-shm | Bin 0 -> 32768 bytes .../Resources/Bookmarks_V2.sqlite-wal | Bin 0 -> 32992 bytes .../Resources/Bookmarks_V3.sqlite | Bin 0 -> 49152 bytes .../Resources/Bookmarks_V3.sqlite-shm | Bin 0 -> 32768 bytes .../Resources/Bookmarks_V3.sqlite-wal | Bin 0 -> 32992 bytes 14 files changed, 554 insertions(+), 67 deletions(-) create mode 100644 Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift create mode 100644 Tests/BookmarksTests/BookmarkMigrationTests.swift create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-shm create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-wal create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-shm create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-wal create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-shm create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-wal diff --git a/Package.swift b/Package.swift index 8f561e30c..a148a6747 100644 --- a/Package.swift +++ b/Package.swift @@ -82,6 +82,13 @@ let package = Package( .process("BookmarksModel.xcdatamodeld") ] ), + .executableTarget(name: "BookmarksTestDBBuilder", + dependencies: [ + "Bookmarks", + "Persistence" + ], + path: "Sources/BookmarksTestDBBuilder" + ), .target( name: "BookmarksTestsUtils", dependencies: [ @@ -234,6 +241,17 @@ let package = Package( dependencies: [ "Bookmarks", "BookmarksTestsUtils" + ], + resources: [ + .copy("Resources/Bookmarks_V1.sqlite"), + .copy("Resources/Bookmarks_V1.sqlite-shm"), + .copy("Resources/Bookmarks_V1.sqlite-wal"), + .copy("Resources/Bookmarks_V2.sqlite"), + .copy("Resources/Bookmarks_V2.sqlite-shm"), + .copy("Resources/Bookmarks_V2.sqlite-wal"), + .copy("Resources/Bookmarks_V3.sqlite"), + .copy("Resources/Bookmarks_V3.sqlite-shm"), + .copy("Resources/Bookmarks_V3.sqlite-wal") ]), .testTarget( name: "BrowserServicesKitTests", diff --git a/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift b/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift index 49fce7802..4f73e8b38 100644 --- a/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift +++ b/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift @@ -28,27 +28,32 @@ public class BookmarkFormFactorFavoritesMigration { public static func getFavoritesOrderFromPreV4Model(dbContainerLocation: URL, dbFileURL: URL, errorEvents: EventMapping? = nil) -> [String]? { - var oldFavoritesOrder: [String]? - let metadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, - at: dbFileURL) - if let metadata, let version = metadata["NSStoreModelVersionHashesVersion"] as? Int, version < 4 { - // Before migrating to latest scheme version, read order of favorites from DB + guard let metadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: dbFileURL), + let latestModel = CoreDataDatabase.loadModel(from: bundle, named: "BookmarksModel"), + !latestModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + else { + return nil + } - let v3BookmarksModel = NSManagedObjectModel.mergedModel(from: [Bookmarks.bundle], forStoreMetadata: metadata)! + // Before migrating to latest scheme version, read order of favorites from DB - let oldDB = CoreDataDatabase(name: "Bookmarks", - containerLocation: dbContainerLocation, - model: v3BookmarksModel) - oldDB.loadStore { context, error in - guard let context = context else { - errorEvents?.fire(.couldNotLoadDatabase, error: error) - return - } + let oldBookmarksModel = NSManagedObjectModel.mergedModel(from: [Bookmarks.bundle], forStoreMetadata: metadata)! + let oldDB = CoreDataDatabase(name: dbFileURL.deletingPathExtension().lastPathComponent, + containerLocation: dbContainerLocation, + model: oldBookmarksModel) - let favs = BookmarkUtils.fetchLegacyFavoritesFolder(context) - oldFavoritesOrder = favs?.favoritesArray.compactMap { $0.uuid } + var oldFavoritesOrder: [String]? + + oldDB.loadStore { context, error in + guard let context = context else { + errorEvents?.fire(.couldNotLoadDatabase, error: error) + return } + + let favs = BookmarkUtils.fetchLegacyFavoritesFolder(context) + let orderedFavorites = favs?.favorites?.array as? [BookmarkEntity] ?? [] + oldFavoritesOrder = orderedFavorites.compactMap { $0.uuid } } return oldFavoritesOrder } diff --git a/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift b/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift new file mode 100644 index 000000000..f452fb3e5 --- /dev/null +++ b/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift @@ -0,0 +1,326 @@ +// +// BookmarksTestDBBuilder.swift +// DuckDuckGo +// +// 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 Cocoa +import Foundation +import CoreData +import Persistence +import Bookmarks + +// swiftlint:disable force_try +// swiftlint:disable line_length +// swiftlint:disable function_body_length + +@main +struct BookmarksTestDBBuilder { + + static func main() { + generateDatabase(modelVersion: 3) + } + + private static func generateDatabase(modelVersion: Int) { + let bundle = Bookmarks.bundle + var momUrl: URL? + if modelVersion == 1 { + momUrl = bundle.url(forResource: "BookmarksModel.momd/BookmarksModel", withExtension: "mom") + } else { + momUrl = bundle.url(forResource: "BookmarksModel.momd/BookmarksModel \(modelVersion)", withExtension: "mom") + } + + guard let dir = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else { + fatalError("Could not find directory") + } + + let model = NSManagedObjectModel(contentsOf: momUrl!) + let stack = CoreDataDatabase(name: "Bookmarks_V\(modelVersion)", + containerLocation: dir, + model: model!) + stack.loadStore() + + let context = stack.makeContext(concurrencyType: .privateQueueConcurrencyType) + context.performAndWait { + buildTestTree(in: context) + } + } + + private static func buildTestTree(in context: NSManagedObjectContext) { + /* When modifying, please add requirements to list below + - Test roof folders (root, favorites) migration and order. + - Test regular folder migration and order. + - Test Form Factor favorites. + */ + let bookmarkTree = BookmarkTree { + Bookmark(id: "1") + Bookmark(id: "2", favoritedOn: [.unified, .mobile]) + Folder(id: "3") { + Folder(id: "31") {} + Bookmark(id: "32", favoritedOn: [.unified, .desktop]) + Bookmark(id: "33", favoritedOn: [.unified, .desktop, .mobile]) + } + Bookmark(id: "4", favoritedOn: [.unified, .desktop, .mobile]) + Bookmark(id: "5", favoritedOn: [.unified, .desktop]) + } + + bookmarkTree.createEntities(in: context) + + // Apply order to make sure order of generation (or PK) does not influence order of results + let unifiedRoot = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.unified.rawValue, + in: context)! + reorderFavorites(to: ["5", "4", "33", "32", "2"], favoritesRoot: unifiedRoot) + + if let desktopRoot = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.desktop.rawValue, + in: context) { + reorderFavorites(to: ["32", "4", "33", "5"], favoritesRoot: desktopRoot) + } + + if let mobileRoot = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.mobile.rawValue, + in: context) { + reorderFavorites(to: ["4", "2", "33"], favoritesRoot: mobileRoot) + } + + try! context.save() + } + + static func reorderFavorites(to ids: [String], favoritesRoot: BookmarkEntity) { + let favs = favoritesRoot.favoritesArray + for fav in favs { + fav.removeFromFavorites(favoritesRoot: favoritesRoot) + } + + for id in ids { + let fav = favs.first(where: { $0.uuid == id}) + fav?.addToFavorites(favoritesRoot: favoritesRoot) + } + } +} + +public enum BookmarkTreeNode { + case bookmark(id: String, name: String?, url: String?, favoritedOn: [FavoritesFolderID], modifiedAt: Date?, isDeleted: Bool, isOrphaned: Bool) + case folder(id: String, name: String?, children: [BookmarkTreeNode], modifiedAt: Date?, isDeleted: Bool, isOrphaned: Bool) + + public var id: String { + switch self { + case .bookmark(let id, _, _, _, _, _, _): + return id + case .folder(let id, _, _, _, _, _): + return id + } + } + + public var name: String? { + switch self { + case .bookmark(_, let name, _, _, _, _, _): + return name + case .folder(_, let name, _, _, _, _): + return name + } + } + + public var modifiedAt: Date? { + switch self { + case .bookmark(_, _, _, _, let modifiedAt, _, _): + return modifiedAt + case .folder(_, _, _, let modifiedAt, _, _): + return modifiedAt + } + } + + public var isDeleted: Bool { + switch self { + case .bookmark(_, _, _, _, _, let isDeleted, _): + return isDeleted + case .folder(_, _, _, _, let isDeleted, _): + return isDeleted + } + } + + public var isOrphaned: Bool { + switch self { + case .bookmark(_, _, _, _, _, _, let isOrphaned): + return isOrphaned + case .folder(_, _, _, _, _, let isOrphaned): + return isOrphaned + } + } +} + +public protocol BookmarkTreeNodeConvertible { + func asBookmarkTreeNode() -> BookmarkTreeNode +} + +public struct Bookmark: BookmarkTreeNodeConvertible { + var id: String + var name: String? + var url: String? + var favoritedOn: [FavoritesFolderID] + var modifiedAt: Date? + var isDeleted: Bool + var isOrphaned: Bool + + public init(_ name: String? = nil, id: String? = nil, url: String? = nil, favoritedOn: [FavoritesFolderID] = [], modifiedAt: Date? = nil, isDeleted: Bool = false, isOrphaned: Bool = false) { + self.id = id ?? UUID().uuidString + self.name = name ?? id + self.url = (url ?? name) ?? id + self.favoritedOn = favoritedOn + self.modifiedAt = modifiedAt + self.isDeleted = isDeleted + self.isOrphaned = isOrphaned + } + + public func asBookmarkTreeNode() -> BookmarkTreeNode { + .bookmark(id: id, name: name, url: url, favoritedOn: favoritedOn, modifiedAt: modifiedAt, isDeleted: isDeleted, isOrphaned: isOrphaned) + } +} + +public struct Folder: BookmarkTreeNodeConvertible { + var id: String + var name: String? + var modifiedAt: Date? + var isDeleted: Bool + var isOrphaned: Bool + var children: [BookmarkTreeNode] + + public init(_ name: String? = nil, id: String? = nil, modifiedAt: Date? = nil, isDeleted: Bool = false, isOrphaned: Bool = false, @BookmarkTreeBuilder children: () -> [BookmarkTreeNode] = { [] }) { + self.id = id ?? UUID().uuidString + self.name = name ?? id + self.modifiedAt = modifiedAt + self.isDeleted = isDeleted + self.isOrphaned = isOrphaned + self.children = children() + } + + public func asBookmarkTreeNode() -> BookmarkTreeNode { + .folder(id: id, name: name, children: children, modifiedAt: modifiedAt, isDeleted: isDeleted, isOrphaned: isOrphaned) + } +} + +@resultBuilder +public struct BookmarkTreeBuilder { + + public static func buildBlock(_ components: BookmarkTreeNodeConvertible...) -> [BookmarkTreeNode] { + components.compactMap { $0.asBookmarkTreeNode() } + } +} + +public struct BookmarkTree { + + public init(modifiedAt: Date? = nil, @BookmarkTreeBuilder builder: () -> [BookmarkTreeNode]) { + self.modifiedAt = modifiedAt + self.bookmarkTreeNodes = builder() + } + + @discardableResult + public func createEntities(in context: NSManagedObjectContext) -> (BookmarkEntity, [BookmarkEntity]) { + let (rootFolder, orphans) = createEntitiesForCheckingModifiedAt(in: context) + return (rootFolder, orphans) + } + + @discardableResult + public func createEntitiesForCheckingModifiedAt(in context: NSManagedObjectContext) -> (BookmarkEntity, [BookmarkEntity]) { + BookmarkUtils.prepareLegacyFoldersStructure(in: context) + + let rootFolder = BookmarkUtils.fetchRootFolder(context)! + rootFolder.modifiedAt = modifiedAt + let favoritesFolders = BookmarkUtils.fetchFavoritesFolders(withUUIDs: Set(FavoritesFolderID.allCases.map(\.rawValue)), in: context) + var orphans = [BookmarkEntity]() + for bookmarkTreeNode in bookmarkTreeNodes { + let entity = BookmarkEntity.makeWithModifiedAtConstraints(with: bookmarkTreeNode, rootFolder: rootFolder, favoritesFolders: favoritesFolders, in: context) + if bookmarkTreeNode.isOrphaned { + orphans.append(entity) + } + } + return (rootFolder, orphans) + } + + let modifiedAt: Date? + let bookmarkTreeNodes: [BookmarkTreeNode] +} + +public extension BookmarkEntity { + @discardableResult + static func make(with treeNode: BookmarkTreeNode, rootFolder: BookmarkEntity, favoritesFolders: [BookmarkEntity], in context: NSManagedObjectContext) -> BookmarkEntity { + makeWithModifiedAtConstraints(with: treeNode, rootFolder: rootFolder, favoritesFolders: favoritesFolders, in: context) + } + + // swiftlint:disable:next cyclomatic_complexity + @discardableResult static func makeWithModifiedAtConstraints(with treeNode: BookmarkTreeNode, rootFolder: BookmarkEntity, favoritesFolders: [BookmarkEntity], in context: NSManagedObjectContext) -> BookmarkEntity { + var entity: BookmarkEntity! + + var queues: [[BookmarkTreeNode]] = [[treeNode]] + var parents: [BookmarkEntity] = [rootFolder] + + while !queues.isEmpty { + var queue = queues.removeFirst() + let parent = parents.removeFirst() + + while !queue.isEmpty { + let node = queue.removeFirst() + + switch node { + case .bookmark(let id, let name, let url, let favoritedOn, let modifiedAt, let isDeleted, let isOrphaned): + let bookmarkEntity = BookmarkEntity(context: context) + if entity == nil { + entity = bookmarkEntity + } + bookmarkEntity.uuid = id + bookmarkEntity.isFolder = false + bookmarkEntity.title = name + bookmarkEntity.url = url + bookmarkEntity.modifiedAt = modifiedAt + + for platform in favoritedOn { + if let favoritesFolder = favoritesFolders.first(where: { $0.uuid == platform.rawValue }) { + bookmarkEntity.addToFavorites(favoritesRoot: favoritesFolder) + } + } + + if isDeleted { + bookmarkEntity.markPendingDeletion() + } + if !isOrphaned { + bookmarkEntity.parent = parent + } + case .folder(let id, let name, let children, let modifiedAt, let isDeleted, let isOrphaned): + let bookmarkEntity = BookmarkEntity(context: context) + if entity == nil { + entity = bookmarkEntity + } + bookmarkEntity.uuid = id + bookmarkEntity.isFolder = true + bookmarkEntity.title = name + bookmarkEntity.modifiedAt = modifiedAt + if isDeleted { + bookmarkEntity.markPendingDeletion() + } + if !isOrphaned { + bookmarkEntity.parent = parent + } + parents.append(bookmarkEntity) + queues.append(children) + } + } + } + + return entity + } +} + +// swiftlint:enable force_try +// swiftlint:enable line_length +// swiftlint:enable function_body_length diff --git a/Tests/BookmarksTests/BookmarkMigrationTests.swift b/Tests/BookmarksTests/BookmarkMigrationTests.swift new file mode 100644 index 000000000..a610105b0 --- /dev/null +++ b/Tests/BookmarksTests/BookmarkMigrationTests.swift @@ -0,0 +1,189 @@ +// +// BookmarkMigrationTests.swift +// DuckDuckGo +// +// 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 BookmarksTestsUtils +import XCTest +import Persistence +@testable import Bookmarks +import Foundation + +class BookmarkMigrationTests: XCTestCase { + + var location: URL! + var resourceURLDir: URL! + + override func setUp() { + super.setUp() + + location = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + guard let location = Bundle(for: BookmarkMigrationTests.self).resourceURL else { + XCTFail("Failed to find bundle URL") + return + } + + let resourcesLocation = location.appendingPathComponent( "BrowserServicesKit_BookmarksTests.bundle/Contents/Resources/") + if FileManager.default.fileExists(atPath: resourcesLocation.path) == false { + resourceURLDir = Bundle.module.resourceURL + } else { + resourceURLDir = resourcesLocation + } + + } + + override func tearDown() { + super.tearDown() + + try? FileManager.default.removeItem(at: location) + } + + func copyDatabase(name: String, formDirectory: URL, toDirectory: URL) throws { + + let fileManager = FileManager.default + try fileManager.createDirectory(at: toDirectory, withIntermediateDirectories: false) + for ext in ["sqlite", "sqlite-shm", "sqlite-wal"] { + + try fileManager.copyItem(at: formDirectory.appendingPathComponent("\(name).\(ext)"), + to: toDirectory.appendingPathComponent("\(name).\(ext)")) + } + } + + func loadDatabase(name: String) -> CoreDataDatabase? { + let bundle = Bookmarks.bundle + guard let model = CoreDataDatabase.loadModel(from: bundle, named: "BookmarksModel") else { + return nil + } + let bookmarksDatabase = CoreDataDatabase(name: name, containerLocation: location, model: model) + bookmarksDatabase.loadStore() + return bookmarksDatabase + } + + func testWhenMigratingFromV1ThenRootFoldersContentsArePreservedInOrder() throws { + throw XCTSkip("Won't run on CI or from command line as momd is not compiled. Tested through Xcode") + try commonMigrationTestForDatabase(name: "Bookmarks_V1") + } + + func testWhenMigratingFromV2ThenRootFoldersContentsArePreservedInOrder() throws { + throw XCTSkip("Won't run on CI or from command line as momd is not compiled. Tested through Xcode") + try commonMigrationTestForDatabase(name: "Bookmarks_V2") + } + + func testWhenMigratingFromV3ThenRootFoldersContentsArePreservedInOrder() throws { + throw XCTSkip("Won't run on CI or from command line as momd is not compiled. Tested through Xcode") + try commonMigrationTestForDatabase(name: "Bookmarks_V3") + } + + func commonMigrationTestForDatabase(name: String) throws { + + try copyDatabase(name: name, formDirectory: resourceURLDir, toDirectory: location) + let legacyFavoritesInOrder = BookmarkFormFactorFavoritesMigration.getFavoritesOrderFromPreV4Model(dbContainerLocation: location, + dbFileURL: location.appendingPathComponent("\(name).sqlite", conformingTo: .database)) + + // Now perform migration and test it + guard let migratedStack = loadDatabase(name: name) else { + XCTFail("Could not initialize legacy stack") + return + } + + let latestContext = migratedStack.makeContext(concurrencyType: .privateQueueConcurrencyType) + latestContext.performAndWait({ + BookmarkFormFactorFavoritesMigration.migrateToFormFactorSpecificFavorites(byCopyingExistingTo: FavoritesFolderID.mobile, + preservingOrderOf: legacyFavoritesInOrder, + in: latestContext) + + let mobileFavoritesArray = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.mobile.rawValue, in: latestContext)?.favoritesArray.compactMap(\.uuid) + XCTAssertEqual(legacyFavoritesInOrder, mobileFavoritesArray) + }) + + // Test whole structure + let bookmarkTree = BookmarkTree { + Bookmark(id: "1") + Bookmark(id: "2", favoritedOn: [.unified, .mobile]) + Folder(id: "3") { + Folder(id: "31") {} + Bookmark(id: "32", favoritedOn: [.unified, .mobile]) + Bookmark(id: "33", favoritedOn: [.unified, .mobile]) + } + Bookmark(id: "4", favoritedOn: [.unified, .mobile]) + Bookmark(id: "5", favoritedOn: [.unified, .mobile]) + } + + latestContext.performAndWait { + let rootFolder = BookmarkUtils.fetchRootFolder(latestContext)! + assertEquivalent(withTimestamps: false, rootFolder, bookmarkTree) + } + + try? migratedStack.tearDown(deleteStores: true) + } + + func atestThatMigrationToFormFactorSpecificFavoritesAddsFavoritesToNativeFolder() async throws { + + guard let bookmarksDatabase = loadDatabase(name: "Any") else { + XCTFail("Failed to load model") + return + } + + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + context.performAndWait { + BookmarkUtils.insertRootFolder(uuid: BookmarkEntity.Constants.rootFolderID, into: context) + BookmarkUtils.insertRootFolder(uuid: FavoritesFolderID.unified.rawValue, into: context) + try! context.save() + } + + let bookmarkTree = BookmarkTree { + Bookmark(id: "1") + Bookmark(id: "2", favoritedOn: [.unified]) + Folder(id: "10") { + Bookmark(id: "12", favoritedOn: [.unified]) + } + Bookmark(id: "3", favoritedOn: [.unified]) + Bookmark(id: "4", favoritedOn: [.unified]) + } + + context.performAndWait { + bookmarkTree.createEntities(in: context) + + try! context.save() + let favoritesArray = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.unified.rawValue, in: context)?.favoritesArray.compactMap(\.uuid) + + BookmarkFormFactorFavoritesMigration.migrateToFormFactorSpecificFavorites(byCopyingExistingTo: FavoritesFolderID.mobile, + preservingOrderOf: nil, + in: context) + + try! context.save() + + let mobileFavoritesArray = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.mobile.rawValue, in: context)?.favoritesArray.compactMap(\.uuid) + XCTAssertEqual(favoritesArray, mobileFavoritesArray) + + let rootFolder = BookmarkUtils.fetchRootFolder(context)! + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + Bookmark(id: "1") + Bookmark(id: "2", favoritedOn: [.mobile, .unified]) + Folder(id: "10") { + Bookmark(id: "12", favoritedOn: [.mobile, .unified]) + } + Bookmark(id: "3", favoritedOn: [.mobile, .unified]) + Bookmark(id: "4", favoritedOn: [.mobile, .unified]) + }) + } + + try? bookmarksDatabase.tearDown(deleteStores: true) + } +} diff --git a/Tests/BookmarksTests/BookmarkUtilsTests.swift b/Tests/BookmarksTests/BookmarkUtilsTests.swift index 8588e4c44..3cd9637cf 100644 --- a/Tests/BookmarksTests/BookmarkUtilsTests.swift +++ b/Tests/BookmarksTests/BookmarkUtilsTests.swift @@ -48,57 +48,6 @@ final class BookmarkUtilsTests: XCTestCase { try? FileManager.default.removeItem(at: location) } - func testThatMigrationToFormFactorSpecificFavoritesAddsFavoritesToNativeFolder() async throws { - - let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) - - context.performAndWait { - BookmarkUtils.insertRootFolder(uuid: BookmarkEntity.Constants.rootFolderID, into: context) - BookmarkUtils.insertRootFolder(uuid: FavoritesFolderID.unified.rawValue, into: context) - try! context.save() - } - - let bookmarkTree = BookmarkTree { - Bookmark(id: "1") - Bookmark(id: "2", favoritedOn: [.unified]) - Folder(id: "10") { - Bookmark(id: "12", favoritedOn: [.unified]) - } - Bookmark(id: "3", favoritedOn: [.unified]) - Bookmark(id: "4", favoritedOn: [.unified]) - } - - context.performAndWait { - bookmarkTree.createEntities(in: context) - - try! context.save() - let favoritesArray = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.unified.rawValue, in: context)?.favoritesArray.compactMap(\.uuid) - - BookmarkFormFactorFavoritesMigration - .migrateToFormFactorSpecificFavorites( - byCopyingExistingTo: .mobile, - preservingOrderOf: nil, - in: context - ) - - try! context.save() - - let mobileFavoritesArray = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.mobile.rawValue, in: context)?.favoritesArray.compactMap(\.uuid) - XCTAssertEqual(favoritesArray, mobileFavoritesArray) - - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Bookmark(id: "1") - Bookmark(id: "2", favoritedOn: [.mobile, .unified]) - Folder(id: "10") { - Bookmark(id: "12", favoritedOn: [.mobile, .unified]) - } - Bookmark(id: "3", favoritedOn: [.mobile, .unified]) - Bookmark(id: "4", favoritedOn: [.mobile, .unified]) - }) - } - } - func testCopyFavoritesWhenDisablingSyncInDisplayNativeMode() async throws { let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite b/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..dbfdd21758093e61fa28c7a87008c716a1b5151a GIT binary patch literal 32768 zcmeI*dpOg30|4;NJyFZGPGmZYxf{b08k@{!n{5_~9WvLU7`c~w4oZcj+(JpIj&ey< zat*15qbR51NJ%d5IVG38Q@k&|_5Sz%anAewKEK)be))XAJkR%;=NZk0=*eNh-TeH0 zoH=k^2n@o<2iXCKLm&`--b;k{S_(qEh`?fEAs@OJUwU6mu6W~zXfox+d09eY6v&#E zH5%e2;__m7yaWh900;m9AOHk_01yBIKmZ8*n<2skg=G~Lp;4Hn4smo07DLDI z8ZE|zzwT@?f1xV|j!MImNpL)`$DKGToI=H0W2p9U0?uBYH?FNM9t&q+sOFX!stOW? zLM&ubhX&EVf93v*al%6G;mVXSdW<1Y+N zv!(DRxmbB2!y03^lte^9_=RP6szIYRdiuJuf*s7rWCCw71YZAmx;@i@f}!%FOgssT zv-_%0aB+g4%iv!Xu51bCX?<0Jw}>x&Bcj&x3Cr%{O~L5fQ?S4=$W%NXXF(=nanx^4 zMc}_r`drB*7BBD`HVOcdbXsqIwty-z`)r$C6ZrIAYaFvBaKv;7B`phib&V?N! zQ~tHPyd<8sG_9rPUot;?EX!J|^#y8K!58Rd8B7Z@f%)C)pG*Iv4evbgeytYIHE#m8 zRO0f~vO^C?e7?MY_s3uU&kf00e*l5C8%|00;m9An?~0kc0|C5F#RGetzCQ&i>vwUydgyOqKfwDlMHT2L}GYzz!0b0Xc z7m3!!;E)D7x;U48Y|j9WwssGck6%DgNLWO4wU{_;9TT>RM5A&1{8`q1t}Hf#EOCollx=Y2b$EW{9kh1f&FASWS}kY31B$TP?w zWC)6ZVxcr>5H#e=(dAqEjy{Q%M;RLS`Tj2w1mQg(00AHX1b_e#00KbZe<*NN7|Nd` zmBI+yC*8eaKHGRkK^O`Py&WrNiSdey-=VbK*UVQ$F;zV*z~CP0bl8=!4}3ii z+jrDa3nJ`xQqp>~3*?(g=Q~Tff01|hijXQ#A)8d>aiKgT@@CkoZ6^$$ zW@q0F>o{<0&iK|pMz@yvs=n&!5{t(0C5Q>C>Lq(!>>mogLElxY9d$4B z=|0u)jC3+rjlb_e1Crn}fg;u6f0c>pk6KgE*yb8u7wOf}=#h*OJm(@b^xUnzd9!+x zya#r5Z>oim{Zsz6=&6>y8!HB!No$VSJ`8WLA3q4lX>JJYl+3r=v@} z`*qF8nwQ&>ajS@FXT=66{hN}m5N8mkzYnShgsVG^O*z)y?=TKgepP8WXW#Qa=G3HJ z4a%h_m7{XY2zQ{g+wpRC+baByb_s*q30AE}n`|==C1%FntJ*|!sC=UN`O7T0Jc8Q-UMkn2o5pxwivSq4gn)W=64yh_PzMEAslU*^)z2}qM=xzK! zQ?B4pR)t-SPP>$b{h>bf015l_4mSA&^Ty^ss>Y6OHjew){0od}et(y+{ZPX(ca4Ay zR-Ba`+F)RJWL`q~Rqr}8?|Y;7G~3=~joBqOCiz{vba;F;GeU~cppu_cZ!r9cq~3@O zmn^qCvhC6x^{&nvqT;TjKU5-7iHFdY*XRY{pwubG@3y6?UlA z@04n}dV2VhOu>cd%LQs%$2GIe%0?a~8>02JuFkcr)7d)7;&;#pkSpq#3A!7|D69@Q za}SFf&?{euiXDCPIDOCQhjCN79?foSjPN#RXEnA`gPqN0qK#^6gQ1P4D)x6>%exqZ z(1%G^Nadik9nr#=<}wcl2s$+lm4E5`Brdk zlu9*eqqyr+B~(nfN&4FwiS;Rs|Hzp!3CcSZHle{6s&FeMyGkKi?pfTk$1z(t#5xnc z@(nYuOR|qZ+r28X%r-C{>RT6$9`bW_>yxhMSCs3S$R4U0i6!Y5#?%h!8Tqcy9kFIz zFe!Cbj^@i36Ti{KE-vcZRCS|3^8|S&+x&fkt#o@K_eC;`VPF4~=-KupdSyX_0Jp3G z{;W9*)*5nlJ!Srr_TT{uRCw^iKr_}9fBl2x#C?v~^Thj#dPk)=$LacMPqepKnK&h* zTDZrlA6@90@X9yh3!Y4nF2T_cO8vi*`Ea zG%gyM@*5=sSuzLZaD-$6`$BsB#6tt7EK2^2&`NKaArTZuV*SNZVln29^+>e~$^zyZ zIoTb7?lqCkY8jYX?+=5;j}j4lLvd6S>pkT4n3;=dt;mcqvZ0e}+t_$$rpK@%b8=9; zWnaJ-57m)!%1sg7Wi$b}T}66^kN8RJ)hE-FCcVwGMQoJ$y4DT0KCj{So7@$g zJ>f8zZ22^0YRc}RXu-{cFQ|_ZXG^C9YmdD6EvmKN8mBfN^T13pzMr{kkpS+>2AR)V8ZOQWi zXU&N15!p_uBaf^c3u2Ta1rOdqhqqf?Lozy4?WIq*__C57i~ic8UuMnL9r;P-+Nowy zwz4GKU}jC&VfmlPSCyYQzn2Tkscr3d87sT?mj8WzAz=$wgX@LR8chqrhZWXx1GogP z=j({qerEzsJQyT=bl5atU302(oYCLZ_fe+*UgczD&a1wuAgAXyKSCojP1~4*w_lx~ z>YLS57<8Yqk32G;nEOh03ioN%?FQ4|b0;2rWSzRVhcT$*AGh5$=f|EF!eG(lwvgHy za=(^BMsCJI#oRR0G%dt&Y6+1cqee(GN;67lS(Q@HU!uH8w{7)em6q-Lm_K=VvUoDn ze@Dn3bm8_QIh!M@&FPY@ovjbjPPFDt9xJ(6?v-6lttB`XU1s!@=#<+yUTNwcEBTEk z+?CL+P;5$7uu-s8psg*rQZiP2kfG&m+spBZ*QcE>z280LiR$I^$*-{7eXf+tF5T5v zB;V!K>D1HTMe;g-y|TQsH!sM-=wd|PQEv0_-awjGXWO4vwN_WUNB&5kb_|WVWx+ge z@aDs7qrm(z?uX&9KZXC8n~s@=&gFfoinKJ%pU2MM_!J*$HJ|k9fH}%41>NMoH)Wk} hRspOg^O?gisTx%+45{|P39j>!N3 literal 0 HcmV?d00001 diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-shm b/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..0dd5f7f2059986ad8328c1fee37ff82f23e5635e GIT binary patch literal 32768 zcmeI)y$J$A5C+io7eNpc;d&4QZNS7bjKm5IjYW&F46zO~3ox|>;hYsYObzwk2g44- zu)!-}irXaRG@}|R4cqbdqDF)5=5f5v4!h;F?62Kx z%A63Wr$F3!6aoYY5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ N009C72oU%&fhU=}C3ye< literal 0 HcmV?d00001 diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-wal b/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-wal new file mode 100644 index 0000000000000000000000000000000000000000..aff9e2d4dabfcf628c4ae150a2ce4b6ad5dfee0a GIT binary patch literal 16512 zcmeI(J!=#}7zglY_H|~G+;eNrw~A-KDDSO^voD_g-vkULNar!Jm{npg-LNF``v zp;)F!>nE@a1OyQjQ4sO1hz4Sz2#Pv0cjpp6K?47|+hz7)Zg%F^?Vr8FH=>(#@Lh|j zPPVNqpU(bF^*%?(R_?Fe?jJR!$Q*J~r?soM9^E;0V8#>$Z<6_z{}fB*y_009U< z00Izz00bbg=LH%fq*RLevg}{z4*JJ04i|=t+HA|WTbtwpk59gT`0D(TS(^*!+7|Ig zUgSgkluy*Q_%3%jnzveF{7x>CQ`4d0q4Ss9K zqd@=y5P$##AOHafKmY;|fB*y_u$u%zUkDWiJ_+TzzU_vtE8W?%h1j07Jq3X;l#N0{ zbNu@s!C5LMoHBq7tqxX_Yk+msZ)6 zR5w&t+)|cSNegaueP>F)U-i}fKE8k6zVGvxG3R{FXL+6HSv)?E@py2wCkHTSSU-A1 zh!+F927y6va3D;uSOfyW$$Im%-d`3JYlUlmgI$iCU;pwszxmAvCt?*<2F)r$p^Om% zR|M41e2^B41@_OS|K0Ra#I0T2KI5C8!X_$LY2qIkrWl#pHwFYjQQ2i1l` zAd=1SW|qXS*ZIvHhh_+5xQ!>RAw{;P(xSSl4iT~0e z(Fsq$JKgCmJzizTsov>-ZQ?Houpcn5c^HPKy_HLkNWiGX#% zJD6GG9aJ>6wQ=l1J2J^}esCs=kC%NkOxO9*=i9TIv+8~Bi?xpWF2VorVP{|;$`8i3 z#oG|SjL#n0(bMFseNmw@L#Bw)4pL4+R|FcF-*;ufq7(w`B&J@r%%JG=wy zDxiKk_upmV{x-|swOlyU=e}`F2@YOyS5^drg(H~bT__GDC!#rpOdvWe7=`Qaqb$_J z!jZnMiQB1-D^WR)zgku>Nu*e*X~p>i%ZmvJ|`j zyWLrvB*!nYzBK<__{rm|qA#^RLw&X5GxS#lRC9_o^@r6z?fthltVaRsIXnNLV+CN} z++S_6udn${Ebi0f{lEJ0Px|!)SwR24Z5C8!X009sH0T2KI5C8!X z00EW&%>Uss009sH0T2KI5C8!X009sH0T2LzpI-px|3CjZh9d+45C8!X009sH0T2KI z5C8!X00Ef)!!-Z`AOHd&00JNY0w4eaAOHd&00KY10L=e?{&NgR2m&Ag0w4eaAOHd& z00JNY0w4eaF#m^Z00ck)1V8`;KmY_l00ck)1V8`;etrS=|Nlb@K0~lxuz&yvfB*=9 z00@8p2!H?xfB*=900{i^1w@eC2pk`uDV-h^;uR4@3}pl`Vn5CQ1w;|77c3wE0w4ea zAOHd&00JNY0w4ea|7!w?n^{vu=Twxoz7ElhV1`%I*V84cX=!Vjso^zs^wre0i0XP8 zx@OwMHEX=Xf&(HM>gsop9GqO-JiL6U#r$ZD5EZk`*3pqckD%GmeQCiiw1~(6dML@4 z#+r5d1+cc(&+m4M4Wm&p!c<#Fd=!K3;^h++6+)&((}JlO*#&yA^ora}izEd2(;^vE zjN}h%P%$#Lj&|%(BN?<%ADVfDR|stjJtAm62^E9=VcYpCM;6(+INFl~7&LoLD#p#4 z;$nkWS6|~ChFyA2-VaH^Pt!p8vZ6AxbfRLIR%5wzipQ#xZRR~ z!S0wC5-v1-J6<`LSP@wGPP!l@?PBjvA+ARlP6Qu99HECGAlwmK5xIy4!~kLtF@zXF zj3Mzz0@4u~jg0+#>2e_FFJa+tFJf9Cv|F6F|aD1R}x6EWNHZ}WDa zcsG1nio*ee8z`?w$rJZkR8_4W2<(p=7)ZV0;qgqU>Z8BkqMKQD69*3tUYstbUz&U0 zH2ZSMa&3N1k=O@z+ONabt*n34TzMepGTml$Z%*jS2KgbszSDaW zf?wRJ-yQedXM(T=wUTMBK70AW6HMF@zO1DB=(FjFts!&aOyL$h1R9Ein~ZVn*6(7DK#B z(p%JfyWSo>?9sa`x=OS9YQe9)zVAx1QXZYk)=F9*FZ$G5xua8Lrs~AH0Ofx0xq;ZHUdt1AGsaeIY4*(HJu)YAJjf@N+Fy^_Fr9t##qrU{dn;0nbk}Y@wb9U6 ztUqJ(i871w^0CL~iYMdqE{T-qx3uYQl2hH;Uc=>|Yl>D)wBDp&nc&g5WECe$v_`ml zy&zgIu+6@GASKMF!>UlS;by7FnGnCH4&mJu!51?%*Y@NUZ@(zIChLzkN~f&QDsELd zuloDyZ{cM<{qAm%c&3lPV?JKwn7cc_@R!19tt?9eiR9Zjf?|bk2+dK0-d}fT%rG~y z`Hpr{>+fm#(}$!HZ-n^Z5mQ^sfv!w-3r5^o2M9oDPo)3 zA6}1;>Ainr=XR2-+4)nVKIhy!(-eC~ca?VEyfP9!d`pT?uBtckWX)~-^16vcfty~2 z5*VgR+vOxZ=`K`W2g+S&rJqt|cFv2$RhA~YZ%l;aCotqyNU-imR9YRmaZgo+^>C?w4qQ|*XJGS40?H`FaLWRox-%JgQz#-Ex(he zZ0_BAs;t;a4%juu6~QFFpkcxc3iNzE8~{!;Zac?o_f{8rSC1tE-{N88IJcg2vdc>y$zk( zHo1Y^w77SowJqwxudRkb>jEX6Zl)gz$K(ehxn$eT9aIDGaywcYx@{$H)@z)avmK94 zIkTmC)!i4D=2}x@ZWUdfPM@vr7*Z=rc8NS&-MRSe_^kZB@XG9?AM2{e5<)KwacjLY zK zFCLkO3LEh1b6lb(Z|+VBrng<2p~WuSK7m9VjZ_v~Ks1E~Xm%Gqtp{`pQSMl)JsBiQ7bFj-?4j^rlz6GvB7cp2eBxdy zzY5bp?dU#od(Hmm9DPD=+;49wiF7G$C{*WqDA^#mKXSwP$6^&(ItnLys1uR@lmp!m zUs4&K9d&NyO0mUvD|@G|F-G&-SAC$zK2#AD+?KadvXFEmL0(+0FJ7=*_=w+mJGD&D z$jG4x+3?Igjd?wiR$w^?O4fUOZBDH}G^%^_Y6;N#U;!=U;Zm zR+eYQOC=%Ux0FS#9Wgz$H16VKq_tZP=&f%P)BSVbGiAe`ySilm1jhMYjgc1>wXZRQ z6Wk__rFypBmz;2q<@u|mQG$}>`tXTA`q276QjD9MCWn?%#`f=dmD}w7A*3d~?ts7n zMd_~kk+l(=XLkiF=w=yo+192$SDMaz-_<%oib!8)b4ad9zDeQb0g(}31c#R05(DYQ!vtga^^=Ikf@6U1$;wSJEorA4|l*ZzS zeUYhW6$b4mB&Ox3r6;r}q9)K2G82lj4;Ih7`j8y2WSm;u>H4bw1IKourljiO!Wpqk z8T9cxQ!*doj9NNp^zOH>|DgO=+@*c@oA0;39Fct>`*7}R(`qg*X9=~Wq_`s+)VVDh zT^b!5tsB=r7UxMCYFReS+hXL^6lS8Q@_{&$I=O4IY%)L0Va9aEK~aAB)KQhIM_2rk zCU@G3e~;nA(C~eYt`{hStCwK}cMI+|7M2y36}m5Cj4>8A7NexiEio1{7Bv=JuN48P!$!_s0Yex5b;%;P) zcR5GYUohFYYWnIg@j1cW?y<)XW>sgt%$3cN%@XXQT(KBAHl;BeFe^FhJ{vpBEp>RE zXc-yj{DjZLMB9U4y;-uzwEKzE+KiOP>(1qEw0ic~%7apU*!7lP&&~BuhU$t1ZVoX7 zhpc6f3;t&D_}ClnKmC(c6tX!^*x_8Pax`g2Yn|MWw24}ub)QYCb=9w2YQJ|yXZFb) z|7`Ib*X&1Et8=Dv1aq2jJ6vr`Ys*XVQOC>6h)*S=YS%g!mDsw9S6KJco<>9^51cyX zY|!^MkdRR}R9+Qor(5P)(NNYvYEQMCTqU1pov_*5@$%fJ2McwPT~eQ#IOH@}ao?#X*^FL}Iv z9-r~liFTvCXg~T9&Hsi`AwYlt0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjY?AdrhkLZD26 z+}aWXWeViVoDe8eAomr8K$!x$EGh)b6v$0oAyB43u1O1lG6iDaT?h~$K!5-N0t5&U gAV7cs0RjXF5FkK+009C72oNAZfB*pk1R5sr4*_B+1poj5 literal 0 HcmV?d00001 diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-wal b/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-wal new file mode 100644 index 0000000000000000000000000000000000000000..17e45ca3174d458a3059877a3c5e6cf77d46ee03 GIT binary patch literal 32992 zcmeI*&r6g+9LMpQ=h|is`?kX!DghCYFqdV-*^WA-(dF^RF zK7)JDM=z*7pHfQIDcdFo^NVfMmtKr-z486%)%JNa6gQ7VW&F{$+Qj8A3yU>oP|H_k zerX6GfB*srAb;0Kr_#ROUnq2)ZtLzkc&6A{?6sMIYo}Jp1@6`y zp8nA9?zFi;ymn5>KDk*tCr9L=_|hR$vRy9PdKv-FG*C`bC4H648UhF)fB*srAb;uweK$v+kE5XP>r2>fN-i-i{CpOEV@15S?-C@#>ugUU)of zbAgzhMI{%w{&HaV)1J01rCeY{$%Oor&oU#kGH$Yg=ki`g)^FCF2>}EUKmY**5I_I{ z1Q0*~0R+|{uu5!D+C^YfgXLWWrIaAtMzEX<*lb`a7dY_v-L+j)w_CYiU=8Y&Abuvx5 literal 0 HcmV?d00001 diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite b/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..494c6d24054f0cd685b90c33fba9d3b73fd1f303 GIT binary patch literal 49152 zcmeI*c{tSD;|K5=+c0)2yN0YaM3yYc9W%oWV`iGME7M>wwy_M^D!U3d(IV1~QX-{f zX_c*nmRlhug|QV9EmtbvQTlzWTlao{&-bry_k5mNKHE8;_j!HJoO#YOo(FXufyrhd zd{|*YbT+~OA_3vyf&79%Kp+ra?o)*O{Jg-qD|`zZ^ZTI->!05jHotn~h3L|hM7g`* zLYpCCrDEcu86tfmtAtbGU2t2W8{C~>0Rlh(2mk>f00e-*KS_WH7m!g`htk<}&p?J7 z&7Oq863{3#4*TW02%3ULIbabEC@TUM@#PVOmKzOCB06A+4hSm($x1s`2qvSV0!=`D zt`O^h!k`>b+(HX$!rxZ5uz$WPCoF}EClL{NZjClr3W7|*+oLG12s^ARl6zf8M?41M zgrcBvD2mo9BO~qkon!)@x^VFXxFBr4H3`lOS6?X4EzUjfQ(fG3_%{juePlkvd{cgK zeIm*p`}z9wm!>+Bxgi%$p5I}Qa{0Weoq&YFWE@vO6Bx{3Z$^|G&5Fee2%?7tV1wCA zb~Md18Xrz(1bZ`seKCwc2AjzWcC#Xp?6?iK<7U7+xC$-g#1k=C7jEr;Ujc$^_e~|- zCgG`MED?hz+HmhJ)?wZkO(GEHZ>SZXIM0isqO}vW1z<9!+_Z5m$>3CbNzkm;U{@ z%)jXe0)L(SPsP9Ai2qC$!V8nJ;YL;X6!q&4NpmBkDBQkD``9DD@#X(IFYDK6RA*`%!(DzvIuW^COqt=fUFJf$objXnt5Bk^gT<`uGv~)b!7Vzx%2A_n`E3 z8@?#?$@_~PG;5L_?T4p-Jo=|H+%F8gH6F_y;t>vbdo0xfWl!b?;tpTjZ;FMXn!7#! zdH-VTi*X#G{c-aC|9bf+eL;h^0s$ZZ1b_e#00KY&2mk>f00e*l5coL+=6{>#{^$=D zAOHk_01yBIKmZ5;0U!VbfB+Bx0zlv=7MTB|elY+4iEl7y8V~>iKmZ5;0U!VbfB+Bx z0zd!=00FK5nE!)r00e*l5C8%|00;m9AOHk_01yBIK;Y*W0Q3K!{~m)D0s$ZZ1b_e# z00KY&2mk>f00e*l5CHRk&f00e*l5C8&T{tx;95C8%|00;m9AOHk_01yBIKmZ5;fuCPs{{R0$ z#h*dA53m3MAOHk_01yBIKmZ5;0U!VbfB+Bx0{{C0(olYgwxFODixm(=4-3EsvzhGZ zkMn=A#SrcTEIf@J|y+^5aew9n;{(CRl5R6!-&^o4Q18dA`14APt6O?C2ATyk;r`HAL;pO8OfC<8dMMNbeX%fmrDwWL& zW7xC28G%lWuy7_T81K#C&N_XV+%5No!w%6Q44Q-#jYvgBuvt!YuZV~s0wafTa6d*khUv=)XVWC)es~5=LWxKv&tEm1%?S2lSclPr7@JsO0SifJ5{Ms`T{xvs zi0njNM_{rU>sHYuT!KQ4FeH{YDr6Hm+#ZW_FlHD-J;hLBKf2MQe<&W?;^+URexu=rg}+~eyx zIY|P+OpmmC7FKjIOC{#q(U5M@1ARW`q4hN1^=-4(!*h|vl6(UaybwW%48#P2fw)3q zA^RXTkOz<+$Rh{`G6Y3IF;FTr5*q!f>+(Psy0FyOT^SntasJP*0O3Bs0tA2n5C8(7 z1on7BdCP1IoP|ObIm&-{%zpFI)H{q<(h3)k(UV9_fba<65yB$+Iq3+wp%FE-hCw5n zq&AWVOOHQcBk=<1j9a6JIB&BDrgdTh-e1Mz%3fUN9ncj}busc$d05-~3$gL>*H2d6 zH8NuLzIZ=3_x|zh;{clBqI)mp42Zl?1-&agu*fGU|JG*|FE>3Yk3GYgGY^ygIHgP4 ziOHW*y&O?W>!QR5J{kzsIM>s5FW88@H}~ZdK}2V>s$@a7W`XFJv#QBHuSLFp*|-|c44=_--TDr?I%}L`d?u#{ zAK5jWGEjJ%8FEDn$D^r+h99wVt8CjK*O?SU?pSk^uK3 zMGeDQhVmjcM$0OCH^_{V8#9H@AIGiGLi1~yW+J2!FqDRsE=L7}6RjB?k5KZjv5wX( z9K+{k0DXp|lI&(z)I ziX0lBXcRKYTR*W!BfH95V^Qn@*IZ5r=dGTPn_cZhr&kNkebYM2_QW#!)vdpALVs0? zzBkbRCW@f3#!agzmYpPi3pv;Bdhm`ZwC>uol@yE1-IeJU$}66jk}G6vk7LH4*T>Zg zEV*JewtjxAI$ixVfL%}6v zqpe8Uj?g8xnsU2T3x3C~QPi_>96z5tbY67QNVmp(f+{Udtm^cJ z!(wi~2>Hb7!&%EWBx`kJo+>xe*?f80CE$>fJnRtvnu;YCpYU4p{gUG3clfZMRKy+cJS!?w-u{@J zVK^kQLd*=oS0Irph@o3Fjrf_R`uzTa%~ODz9`QEYK13~6d1_P?*f1pt-&l6;uBrj~ zXqVTRN|mm%xIkW_o1@wyvX@SI?@1i8q^y&6#HggBqz@uRElhQ2eC9ZO%x@wJ$zBo@ z-=J|n1NBa%`pS)p#npwsR5$8W@Y=qR6R#<(R32$|5?*@&QP)ZJq9n`ROQN4j@Mn~c z#qL4u-{7F!RZXwG(*ZqVQcCF}n%i@Cts~y6pOr0=eEX=8?{SiOgWzQuGHhKsC(M(j@%er*sH&+Jq;@7eg-;cb4M)6o!Zo|h)9kgQEg<76!ey-~hAg^`z zmCJ3Jt`332cRcQ&9g>WopEzk=S}m(s9W$Wv=)h>G&l4YGw+MZfOF(9mS%*vFOlek) zePcHE+V3l}_v@%qpd}ISj1huPE7Mbb`%I+=6ZhMhnDC{EidoCfZ51@(v61GBvgf%t zLW?{zUNJ~J^8#Dwxw&~msHCsu-WGdw2r<`0d(upGhM>4qX?Q)UGIX<;YVi|RYgO}B ztN8bV`tgYTkQdcWQ;Yssb|9g~e)YTB`#Xv8({0{9JU5KqZHt!=AgxbdR~v-HC2icf z%ye}=*`oH)qGhwNs~zc~moxcZc^;?s#fH;dn$6?msao4)*}69*PmdLw8}(gMDY0xa zY$2kWmT7n@tbLr1N!YcPS1rd!|CGeed~9L)5g@rs>(XcW`Fc z58d^X)G3@EX+W(?%}{q8iI*<0R~abWP0(9!tXN&m;}Ph8G3s?R+Q(OP;`yPajKE;2 z%Cc2QB0Wtz^$>@g3uA?Hq+hhFpF5lN04BLbVL#z$RvZPge7`NF@J)XKQ)|cRhq?-K zMIr2R!nAu;KGEhwJW={a$aQw2g3mj>ZRovkcB%gHxDt8Mb5(%1<=bu1TjiKhg5}=E zcijAM+xn_nGz(aWUY>aS_9;BQn4f&xW=S@dz2wr{u%mG;4)QNOk7%F8y;Di=bUCkY z+|&<0TYKlMiGt2eePrk1<_Cj&>&+TBS}+e4vnK`x9!eNK;=6G~y-}7xl0}|tI^&mK<_n~-R{$4s#Bty z-EDJHQ^8M8=a_K$WrYgy^qd_Bihp~4*qWyBn&Gxt;N=bPvgnQOn*Hk6ANiq*p4YBwE{9-3aWhXYd)AB%Nd_}%`@vT z>-c)~wdyQk78$$!(mju0*6Fg>_OEBhRzAGdUDqe~0lo!JyFwZio*9jkq_xv-+#0f* z!NwUqA4q!V^I^*e**W;!@;QMyjkzUTcP!=HntVPvoUEqeid%dqg+R~9QqPb+lcK2N z?4nznEMMqUA|s#T*x*mkd?NWE1(uSQe95YAa@%BX1xZ#yOc0hLp0YEAlyWQiR?5oc zQ^~C4I^oCgIVs`hbNz9l3AH$+v`qS*eyPyoe@KU(86_>R>5aV3e=pK}yNn1)-=0q_wcB^ORoGDh+cFxnV-?!t4 zC-XQbCwO>;)lf^%wz02HqLDMX2fRI$}?KSt>K(=Q*qG=7je4#$_Cxet@BVn zwYRH9-*w*wj%Zj-+=ZSMkF_7GKi>=E@J^v-v}c^BSe#+bKF+|D(2UWH=*+Dt8b^F^ z=OBsmd~uN;{Mh|HNXCKea*o!yRyD>!N+EJx=^jR_Tk|o#-A={!+m~CkIt_{q>JA1E zdJJwUrga|^(Sq$xYQ5V^ALJh_nJStpol2N;9yEsy%Rbi%*=?Suw8f_E@T1c=fA71k dq@ZN+>-eu}isDH#r@K$k!j`U|3RQe7{x6zr@3;T} literal 0 HcmV?d00001 diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-shm b/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..0d1100a08f09b153cda173e17ee086595c0278ff GIT binary patch literal 32768 zcmeI)J!%3`6b0bX_!}$)o7sVtjfDh4vIa{*+<{=@Hr#`i3$RX?E=#b?CQ`LAVVHTvw}4asPO3~Z+Ni2wFU~>q`)7SvzJ2aKR)43(aXj^8)i*{FW-`?%_o zy!Zbu*Vnh>HJ)Cgt!O*iiFTv;Uq31Y2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfWR{d zPKC{rM}wuC^L0=Y6L1j-c1eMKQqra&%>3V|{Oa#L3blqryF(n6q2f!KE!0t5&U nAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5;&ClfdW3HcF|90%|>`(|d}EVe(FJS>z59SWm@-PtAk3l$wa6n2TgqKr~Rb72!9D1r_J zLe|YgAb}uW`a=+S=}-~HM)W^Y%RoDnBt=9f4!l&)N4r>*alU zGp;=99uv#QlR{Jq-q!7!9@%l}Y{P}IJq<(6lUwwunEt5<&o^%US}#rg}K1s z;ak0jzBQcYTp$+j7t~8b@rigpEhC~G)D^!*&$vA{2tWV=5P$##AOHafKmY;|fWSWz zsJ7$6^E@Ri5s4;5!c}6!jqZ-;$u-k{%1_mJT&d39uks4BKt-IK{=mdfJq zbAM|^?cUQ{rEewx1) z#eQ#7saFjIcB{_#X451#yM(#G?d{Di_rJfX=fEIF1woIgKrd*7Uea6oK(Fa8edhMq zAOHafKmY;|fB*y_009U<00IywCjnau%XXw?IUesyE?h1gE~2JJ+0ybHvl}rbPyZbr zMVJfh9U5G+dtl$8~Jld>}2d|N*_u=IfldQE_?@?BF^ z`iKL^RHdTkC953W&`SqbOhB3x~y>Wa~e@p&&kPBQ9`so5!=n363`2c4F z*J)x-Qe#jMfB*y_009U<00Izz00bZaf!QYD*g`K4D24{+K$_Au<)7vPo*wqk|5G^7 wUbud8eO+Ua3*bJ2+4d)a{z3o(5P$##AOHafKmY;|fWUt(!1o6H)qMm%0Q?E{TmS$7 literal 0 HcmV?d00001 From 023a764d9ff905cf3e4205f49bc2b643cca943e0 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 27 Nov 2023 18:23:54 +0100 Subject: [PATCH 03/39] Merge 83.0.0-4, 84.1.1-2 and 85.0.0-1 hotfixes into main (#579) Task/Issue URL: https://app.asana.com/0/414235014887631/1206033476164720 From 1331652ad0dc21c23b495b4a9a42e2a0eb44859d Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Tue, 28 Nov 2023 03:03:30 +0100 Subject: [PATCH 04/39] Adds support for managing VPN settings. (#565) Task/Issue URL: https://app.asana.com/0/0/1205958731729757/f iOS PR: duckduckgo/iOS#2165 macOS PR: duckduckgo/macos-browser#1858 What kind of version bump will this require?: Major/Minor/Patch Description Adds support for the settings we'll be showing in our macOS Settings pane. --- Sources/NetworkProtection/AppLaunching.swift | 1 + .../ExtensionMessage/ExtensionRequest.swift | 2 +- ...kProtectionCodeRedemptionCoordinator.swift | 2 +- .../NetworkProtectionDeviceManager.swift | 2 +- .../NetworkProtectionOptionKey.swift | 1 - .../Networking/NetworkProtectionClient.swift | 2 +- .../PacketTunnelProvider.swift | 76 +++++----- ...workProtectionLocationListRepository.swift | 2 +- .../UserDefaults+connectOnLogin.swift | 45 ++++++ .../UserDefaults+excludeLocalNetworks.swift | 4 +- .../UserDefaults+notifyStatusChanges.swift | 47 +++++++ ...UserDefaults+registrationKeyValidity.swift | 6 +- .../UserDefaults+selectedEnvironment.swift | 10 +- .../UserDefaults+selectedLocation.swift | 6 +- .../UserDefaults+selectedServer.swift | 6 +- .../UserDefaults+showInMenuBar.swift | 47 +++++++ .../Settings/RoutingRange.swift | 57 ++++++++ ...TunnelSettings.swift => VPNSettings.swift} | 131 ++++++++++++++---- .../NetworkProtection/StartupOptions.swift | 10 +- ...ficationsPresenterTogglableDecorator.swift | 39 +++--- ...ProtectionNotificationsSettingsStore.swift | 48 ------- ...ProtectionNotificationsSettingsStore.swift | 25 ---- .../NetworkProtectionClientTests.swift | 4 +- 23 files changed, 379 insertions(+), 194 deletions(-) create mode 100644 Sources/NetworkProtection/Settings/Extensions/UserDefaults+connectOnLogin.swift create mode 100644 Sources/NetworkProtection/Settings/Extensions/UserDefaults+notifyStatusChanges.swift create mode 100644 Sources/NetworkProtection/Settings/Extensions/UserDefaults+showInMenuBar.swift create mode 100644 Sources/NetworkProtection/Settings/RoutingRange.swift rename Sources/NetworkProtection/Settings/{TunnelSettings.swift => VPNSettings.swift} (72%) delete mode 100644 Sources/NetworkProtection/Storage/NetworkProtectionNotificationsSettingsStore.swift delete mode 100644 Sources/NetworkProtectionTestUtils/Storage/MockNetworkProtectionNotificationsSettingsStore.swift diff --git a/Sources/NetworkProtection/AppLaunching.swift b/Sources/NetworkProtection/AppLaunching.swift index 1909c7ee8..07d7bb7ec 100644 --- a/Sources/NetworkProtection/AppLaunching.swift +++ b/Sources/NetworkProtection/AppLaunching.swift @@ -25,6 +25,7 @@ public enum AppLaunchCommand: Codable { case justOpen case shareFeedback case showStatus + case showSettings case startVPN case stopVPN case enableOnDemand diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift index 0b26465ce..fbd8accf5 100644 --- a/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionRequest.swift @@ -26,6 +26,6 @@ public enum DebugCommand: Codable { } public enum ExtensionRequest: Codable { - case changeTunnelSetting(_ change: TunnelSettings.Change) + case changeTunnelSetting(_ change: VPNSettings.Change) case debugCommand(_ command: DebugCommand) } diff --git a/Sources/NetworkProtection/FeatureActivation/NetworkProtectionCodeRedemptionCoordinator.swift b/Sources/NetworkProtection/FeatureActivation/NetworkProtectionCodeRedemptionCoordinator.swift index acecf18df..0facf0aa1 100644 --- a/Sources/NetworkProtection/FeatureActivation/NetworkProtectionCodeRedemptionCoordinator.swift +++ b/Sources/NetworkProtection/FeatureActivation/NetworkProtectionCodeRedemptionCoordinator.swift @@ -32,7 +32,7 @@ public final class NetworkProtectionCodeRedemptionCoordinator: NetworkProtection private let versionStore: NetworkProtectionLastVersionRunStore private let errorEvents: EventMapping - convenience public init(environment: TunnelSettings.SelectedEnvironment, + convenience public init(environment: VPNSettings.SelectedEnvironment, tokenStore: NetworkProtectionTokenStore, versionStore: NetworkProtectionLastVersionRunStore = .init(), errorEvents: EventMapping) { diff --git a/Sources/NetworkProtection/NetworkProtectionDeviceManager.swift b/Sources/NetworkProtection/NetworkProtectionDeviceManager.swift index cb9a3c577..34482ac2f 100644 --- a/Sources/NetworkProtection/NetworkProtectionDeviceManager.swift +++ b/Sources/NetworkProtection/NetworkProtectionDeviceManager.swift @@ -44,7 +44,7 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement { private let errorEvents: EventMapping? - public init(environment: TunnelSettings.SelectedEnvironment, + public init(environment: VPNSettings.SelectedEnvironment, tokenStore: NetworkProtectionTokenStore, keyStore: NetworkProtectionKeyStore, serverListStore: NetworkProtectionServerListStore? = nil, diff --git a/Sources/NetworkProtection/NetworkProtectionOptionKey.swift b/Sources/NetworkProtection/NetworkProtectionOptionKey.swift index 086a1700a..e5b9f7c6e 100644 --- a/Sources/NetworkProtection/NetworkProtectionOptionKey.swift +++ b/Sources/NetworkProtection/NetworkProtectionOptionKey.swift @@ -29,6 +29,5 @@ public enum NetworkProtectionOptionKey { public static let tunnelFatalErrorCrashSimulation = "tunnelFatalErrorCrashSimulation" public static let tunnelMemoryCrashSimulation = "tunnelMemoryCrashSimulation" public static let includedRoutes = "includedRoutes" - public static let excludedRoutes = "excludedRoutes" public static let connectionTesterEnabled = "connectionTesterEnabled" } diff --git a/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift b/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift index ade571ef4..6b87993fc 100644 --- a/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift +++ b/Sources/NetworkProtection/Networking/NetworkProtectionClient.swift @@ -141,7 +141,7 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient { private let endpointURL: URL - init(environment: TunnelSettings.SelectedEnvironment = .default) { + init(environment: VPNSettings.SelectedEnvironment = .default) { endpointURL = environment.endpointURL } diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 937977f73..04cbcb202 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -113,7 +113,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { // MARK: - Tunnel Settings - private let settings: TunnelSettings + private let settings: VPNSettings // MARK: - Server Selection @@ -126,7 +126,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { public let lastSelectedServerInfoPublisher = CurrentValueSubject.init(nil) private var includedRoutes: [IPAddressRange]? - private var excludedRoutes: [IPAddressRange]? // MARK: - User Notifications @@ -300,7 +299,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { tokenStore: NetworkProtectionTokenStore, debugEvents: EventMapping?, providerEvents: EventMapping, - tunnelSettings: TunnelSettings = TunnelSettings(defaults: .standard)) { + settings: VPNSettings) { os_log("[+] PacketTunnelProvider", log: .networkProtectionMemoryLog, type: .debug) self.notificationsPresenter = notificationsPresenter @@ -310,11 +309,11 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { self.providerEvents = providerEvents self.tunnelHealth = tunnelHealthStore self.controllerErrorStore = controllerErrorStore - self.settings = tunnelSettings + self.settings = settings super.init() - tunnelSettings.changePublisher + settings.changePublisher .receive(on: DispatchQueue.main) .sink { [weak self] change in self?.handleSettingsChange(change) @@ -428,17 +427,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { private func loadRoutes(from options: [String: Any]?) { self.includedRoutes = (options?[NetworkProtectionOptionKey.includedRoutes] as? [String])?.compactMap(IPAddressRange.init(from:)) ?? [] - - self.excludedRoutes = (options?[NetworkProtectionOptionKey.excludedRoutes] as? [String])?.compactMap(IPAddressRange.init(from:)) - ?? [ // fallback to default local network exclusions - "10.0.0.0/8", // 255.0.0.0 - "172.16.0.0/12", // 255.240.0.0 - "192.168.0.0/16", // 255.255.0.0 - "169.254.0.0/16", // 255.255.0.0 : Link-local - "127.0.0.0/8", // 255.0.0.0 : Loopback - "224.0.0.0/4", // 240.0.0.0 : Multicast - "100.64.0.0/16", // 255.255.0.0 : Shared Address Space - ] } // MARK: - Tunnel Start @@ -524,14 +512,15 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { return serverSelectionMethod } - private func startTunnel(environment: TunnelSettings.SelectedEnvironment, onDemand: Bool, completionHandler: @escaping (Error?) -> Void) { + private func startTunnel(environment: VPNSettings.SelectedEnvironment, onDemand: Bool, completionHandler: @escaping (Error?) -> Void) { Task { do { os_log("🔵 Generating tunnel config", log: .networkProtection, type: .info) + os_log("🔵 Excluded ranges are: %{public}@", log: .networkProtection, type: .info, String(describing: settings.excludedRanges)) let tunnelConfiguration = try await generateTunnelConfiguration(environment: environment, serverSelectionMethod: currentServerSelectionMethod, includedRoutes: includedRoutes ?? [], - excludedRoutes: excludedRoutes ?? []) + excludedRoutes: settings.excludedRanges) startTunnel(with: tunnelConfiguration, onDemand: onDemand, completionHandler: completionHandler) os_log("🔵 Done generating tunnel config", log: .networkProtection, type: .info) } catch { @@ -584,9 +573,12 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } Task { [weak self] in - await self?.handleAdapterStopped() - if case .superceded = reason { - self?.notificationsPresenter.showSupersededNotification() + if let self { + await self.handleAdapterStopped() + + if case .superceded = reason { + self.notificationsPresenter.showSupersededNotification() + } } completionHandler() @@ -660,12 +652,12 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } @MainActor - public func updateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment, serverSelectionMethod: NetworkProtectionServerSelectionMethod, reassert: Bool = true) async throws { + public func updateTunnelConfiguration(environment: VPNSettings.SelectedEnvironment = .default, serverSelectionMethod: NetworkProtectionServerSelectionMethod, reassert: Bool = true) async throws { let tunnelConfiguration = try await generateTunnelConfiguration(environment: environment, serverSelectionMethod: serverSelectionMethod, includedRoutes: includedRoutes ?? [], - excludedRoutes: excludedRoutes ?? []) + excludedRoutes: settings.excludedRanges) try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation) in guard let self = self else { @@ -696,7 +688,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } @MainActor - private func generateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment, serverSelectionMethod: NetworkProtectionServerSelectionMethod, includedRoutes: [IPAddressRange], excludedRoutes: [IPAddressRange]) async throws -> TunnelConfiguration { + private func generateTunnelConfiguration(environment: VPNSettings.SelectedEnvironment = .default, serverSelectionMethod: NetworkProtectionServerSelectionMethod, includedRoutes: [IPAddressRange], excludedRoutes: [IPAddressRange]) async throws -> TunnelConfiguration { let configurationResult: (TunnelConfiguration, NetworkProtectionServerInfo) @@ -757,8 +749,9 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { handleResetAllState(completionHandler: completionHandler) case .triggerTestNotification: handleSendTestNotification(completionHandler: completionHandler) - case .setExcludedRoutes(let excludedRoutes): - setExcludedRoutes(excludedRoutes, completionHandler: completionHandler) + case .setExcludedRoutes: + // No longer supported, will remove, but keeping the enum to prevent ABI issues + completionHandler?(nil) case .setIncludedRoutes(let includedRoutes): setIncludedRoutes(includedRoutes, completionHandler: completionHandler) case .simulateTunnelFailure: @@ -783,13 +776,21 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } } - private func handleSettingChangeAppRequest(_ change: TunnelSettings.Change, completionHandler: ((Data?) -> Void)? = nil) { + private func handleSettingChangeAppRequest(_ change: VPNSettings.Change, completionHandler: ((Data?) -> Void)? = nil) { settings.apply(change: change) handleSettingsChange(change, completionHandler: completionHandler) } - private func handleSettingsChange(_ change: TunnelSettings.Change, completionHandler: ((Data?) -> Void)? = nil) { + // swiftlint:disable:next cyclomatic_complexity + private func handleSettingsChange(_ change: VPNSettings.Change, completionHandler: ((Data?) -> Void)? = nil) { switch change { + case .setExcludeLocalNetworks: + Task { + if case .connected = connectionStatus { + try? await updateTunnelConfiguration(reassert: false) + } + completionHandler?(nil) + } case .setSelectedServer(let selectedServer): let serverSelectionMethod: NetworkProtectionServerSelectionMethod @@ -822,11 +823,13 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } completionHandler?(nil) } - case .setIncludeAllNetworks, + case .setConnectOnLogin, + .setIncludeAllNetworks, .setEnforceRoutes, - .setExcludeLocalNetworks, + .setNotifyStatusChanges, .setRegistrationKeyValidity, - .setSelectedEnvironment: + .setSelectedEnvironment, + .setShowInMenuBar: // Intentional no-op, as some setting changes don't require any further operation break } @@ -935,17 +938,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { completionHandler?(nil) } - private func setExcludedRoutes(_ excludedRoutes: [IPAddressRange], completionHandler: ((Data?) -> Void)? = nil) { - Task { - self.excludedRoutes = excludedRoutes - - if case .connected = connectionStatus { - try? await updateTunnelConfiguration(reassert: false) - } - completionHandler?(nil) - } - } - private func setIncludedRoutes(_ includedRoutes: [IPAddressRange], completionHandler: ((Data?) -> Void)? = nil) { Task { self.includedRoutes = includedRoutes diff --git a/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift b/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift index 46d64efc6..773aac8df 100644 --- a/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift +++ b/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift @@ -27,7 +27,7 @@ final public class NetworkProtectionLocationListCompositeRepository: NetworkProt private let client: NetworkProtectionClient private let tokenStore: NetworkProtectionTokenStore - convenience public init(environment: TunnelSettings.SelectedEnvironment, tokenStore: NetworkProtectionTokenStore) { + convenience public init(environment: VPNSettings.SelectedEnvironment, tokenStore: NetworkProtectionTokenStore) { self.init( client: NetworkProtectionBackendClient(environment: environment), tokenStore: tokenStore diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+connectOnLogin.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+connectOnLogin.swift new file mode 100644 index 000000000..865a90234 --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+connectOnLogin.swift @@ -0,0 +1,45 @@ +// +// UserDefaults+connectOnLogin.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 Combine +import Foundation + +extension UserDefaults { + private var connectOnLoginKey: String { + "networkProtectionSettingConnectOnLogin" + } + + @objc + dynamic var networkProtectionSettingConnectOnLogin: Bool { + get { + bool(forKey: connectOnLoginKey) + } + + set { + set(newValue, forKey: connectOnLoginKey) + } + } + + var networkProtectionSettingConnectOnLoginPublisher: AnyPublisher { + publisher(for: \.networkProtectionSettingConnectOnLogin).eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingConnectOnLogin() { + removeObject(forKey: connectOnLoginKey) + } +} diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+excludeLocalNetworks.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+excludeLocalNetworks.swift index 75df3458d..c5795fb97 100644 --- a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+excludeLocalNetworks.swift +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+excludeLocalNetworks.swift @@ -24,10 +24,12 @@ extension UserDefaults { "networkProtectionSettingExcludeLocalNetworks" } + static let excludeLocalNetworksDefaultValue = true + @objc dynamic var networkProtectionSettingExcludeLocalNetworks: Bool { get { - bool(forKey: excludeLocalNetworksKey) + value(forKey: excludeLocalNetworksKey) as? Bool ?? Self.excludeLocalNetworksDefaultValue } set { diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+notifyStatusChanges.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+notifyStatusChanges.swift new file mode 100644 index 000000000..1b3dc2912 --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+notifyStatusChanges.swift @@ -0,0 +1,47 @@ +// +// UserDefaults+notifyStatusChanges.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 Combine +import Foundation + +extension UserDefaults { + private var notifyStatusChangesKey: String { + "networkProtectionNotifyStatusChanges" + } + + private static let notifyStatusChangesDefaultValue = true + + @objc + dynamic var networkProtectionNotifyStatusChanges: Bool { + get { + value(forKey: notifyStatusChangesKey) as? Bool ?? Self.notifyStatusChangesDefaultValue + } + + set { + set(newValue, forKey: notifyStatusChangesKey) + } + } + + var networkProtectionNotifyStatusChangesPublisher: AnyPublisher { + publisher(for: \.networkProtectionNotifyStatusChanges).eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingNotifyStatusChanges() { + removeObject(forKey: notifyStatusChangesKey) + } +} diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+registrationKeyValidity.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+registrationKeyValidity.swift index 63ca7ddc8..d8ff8ab60 100644 --- a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+registrationKeyValidity.swift +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+registrationKeyValidity.swift @@ -35,7 +35,7 @@ extension UserDefaults { } } - private func registrationKeyValidityFromRawValue(_ rawValue: NSNumber?) -> TunnelSettings.RegistrationKeyValidity { + private func registrationKeyValidityFromRawValue(_ rawValue: NSNumber?) -> VPNSettings.RegistrationKeyValidity { guard let timeInterval = networkProtectionSettingRegistrationKeyValidityRawValue?.doubleValue else { return .automatic } @@ -43,7 +43,7 @@ extension UserDefaults { return .custom(timeInterval) } - var networkProtectionSettingRegistrationKeyValidity: TunnelSettings.RegistrationKeyValidity { + var networkProtectionSettingRegistrationKeyValidity: VPNSettings.RegistrationKeyValidity { get { registrationKeyValidityFromRawValue(networkProtectionSettingRegistrationKeyValidityRawValue) } @@ -58,7 +58,7 @@ extension UserDefaults { } } - var networkProtectionSettingRegistrationKeyValidityPublisher: AnyPublisher { + var networkProtectionSettingRegistrationKeyValidityPublisher: AnyPublisher { let registrationKeyValidityFromRawValue = self.registrationKeyValidityFromRawValue return publisher(for: \.networkProtectionSettingRegistrationKeyValidityRawValue).map { serverName in diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedEnvironment.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedEnvironment.swift index 4d8daf78a..b5006f9a3 100644 --- a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedEnvironment.swift +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedEnvironment.swift @@ -27,7 +27,7 @@ extension UserDefaults { @objc dynamic var networkProtectionSettingSelectedEnvironmentRawValue: String { get { - value(forKey: selectedEnvironmentKey) as? String ?? TunnelSettings.SelectedEnvironment.default.rawValue + value(forKey: selectedEnvironmentKey) as? String ?? VPNSettings.SelectedEnvironment.default.rawValue } set { @@ -35,9 +35,9 @@ extension UserDefaults { } } - var networkProtectionSettingSelectedEnvironment: TunnelSettings.SelectedEnvironment { + var networkProtectionSettingSelectedEnvironment: VPNSettings.SelectedEnvironment { get { - TunnelSettings.SelectedEnvironment(rawValue: networkProtectionSettingSelectedEnvironmentRawValue) ?? .default + VPNSettings.SelectedEnvironment(rawValue: networkProtectionSettingSelectedEnvironmentRawValue) ?? .default } set { @@ -45,9 +45,9 @@ extension UserDefaults { } } - var networkProtectionSettingSelectedEnvironmentPublisher: AnyPublisher { + var networkProtectionSettingSelectedEnvironmentPublisher: AnyPublisher { publisher(for: \.networkProtectionSettingSelectedEnvironmentRawValue).map { value in - TunnelSettings.SelectedEnvironment(rawValue: value) ?? .default + VPNSettings.SelectedEnvironment(rawValue: value) ?? .default }.eraseToAnyPublisher() } diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedLocation.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedLocation.swift index 82a543f60..1b082c492 100644 --- a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedLocation.swift +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedLocation.swift @@ -52,7 +52,7 @@ extension UserDefaults { } } - private static func selectedLocationFromStorageValue(_ storageValue: StorableLocation?) -> TunnelSettings.SelectedLocation { + private static func selectedLocationFromStorageValue(_ storageValue: StorableLocation?) -> VPNSettings.SelectedLocation { guard let storageValue else { return .nearest } @@ -61,7 +61,7 @@ extension UserDefaults { return .location(selectedLocation) } - var networkProtectionSettingSelectedLocation: TunnelSettings.SelectedLocation { + var networkProtectionSettingSelectedLocation: VPNSettings.SelectedLocation { get { Self.selectedLocationFromStorageValue(networkProtectionSettingSelectedLocationStorageValue) } @@ -76,7 +76,7 @@ extension UserDefaults { } } - var networkProtectionSettingSelectedLocationPublisher: AnyPublisher { + var networkProtectionSettingSelectedLocationPublisher: AnyPublisher { return publisher(for: \.networkProtectionSettingSelectedLocationStorageValue) .map(Self.selectedLocationFromStorageValue(_:)) .eraseToAnyPublisher() diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedServer.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedServer.swift index 4c2ad0246..bfefeabc0 100644 --- a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedServer.swift +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+selectedServer.swift @@ -35,7 +35,7 @@ extension UserDefaults { } } - private func selectedServerFromRawValue(_ rawValue: String?) -> TunnelSettings.SelectedServer { + private func selectedServerFromRawValue(_ rawValue: String?) -> VPNSettings.SelectedServer { guard let selectedEndpoint = networkProtectionSettingSelectedServerRawValue else { return .automatic } @@ -43,7 +43,7 @@ extension UserDefaults { return .endpoint(selectedEndpoint) } - var networkProtectionSettingSelectedServer: TunnelSettings.SelectedServer { + var networkProtectionSettingSelectedServer: VPNSettings.SelectedServer { get { selectedServerFromRawValue(networkProtectionSettingSelectedServerRawValue) } @@ -58,7 +58,7 @@ extension UserDefaults { } } - var networkProtectionSettingSelectedServerPublisher: AnyPublisher { + var networkProtectionSettingSelectedServerPublisher: AnyPublisher { let selectedServerFromRawValue = self.selectedServerFromRawValue return publisher(for: \.networkProtectionSettingSelectedServerRawValue).map { serverName in diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showInMenuBar.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showInMenuBar.swift new file mode 100644 index 000000000..e9faeb10d --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+showInMenuBar.swift @@ -0,0 +1,47 @@ +// +// UserDefaults+showInMenuBar.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 Combine +import Foundation + +extension UserDefaults { + private var showInMenuBarKey: String { + "networkProtectionSettingShowInMenuBar" + } + + static let showInMenuBarDefaultValue = true + + @objc + dynamic var networkProtectionSettingShowInMenuBar: Bool { + get { + value(forKey: showInMenuBarKey) as? Bool ?? Self.showInMenuBarDefaultValue + } + + set { + set(newValue, forKey: showInMenuBarKey) + } + } + + var networkProtectionSettingShowInMenuBarPublisher: AnyPublisher { + publisher(for: \.networkProtectionSettingShowInMenuBar).eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingShowInMenuBar() { + removeObject(forKey: showInMenuBarKey) + } +} diff --git a/Sources/NetworkProtection/Settings/RoutingRange.swift b/Sources/NetworkProtection/Settings/RoutingRange.swift new file mode 100644 index 000000000..2b5582a37 --- /dev/null +++ b/Sources/NetworkProtection/Settings/RoutingRange.swift @@ -0,0 +1,57 @@ +// +// RoutingRange.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 + +public enum RoutingRange { + case section(String) + case range(_ range: NetworkProtection.IPAddressRange, description: String? = nil) + + public static let alwaysExcludedIPv4Ranges: [RoutingRange] = [ + .section("IPv4 - Always Excluded"), + .range("10.0.0.0/8" /* 255.0.0.0 */, description: "disabled for enforceRoutes"), + .range("100.64.0.0/16" /* 255.255.0.0 */, description: "Shared Address Space"), + .range("127.0.0.0/8" /* 255.0.0.0 */, description: "Loopback"), + .range("169.254.0.0/16" /* 255.255.0.0 */, description: "Link-local"), + .range("224.0.0.0/4" /* 240.0.0.0 */, description: "Multicast"), + .range("240.0.0.0/8" /* 255.0.0.0 */, description: "Multicast"), + + .section("duckduckgo.com"), + .range("52.142.124.215/32"), + .range("52.250.42.157/32"), + .range("40.114.177.156/32"), + ] + + public static let alwaysExcludedIPv6Ranges: [RoutingRange] = [ + // When need to figure out what will happen to these when + // excludeLocalNetworks is OFF. + // For now though, I'm keeping these but leaving these always excluded + // as IPv6 is out of scope. + .section("IPv6 - Always Excluded"), + .range("fe80::/10", description: "link local"), + .range("ff00::/8", description: "multicast"), + .range("fc00::/7", description: "local unicast"), + .range("::1/128", description: "loopback"), + ] + + public static let localNetworkRanges: [RoutingRange] = [ + .section("IPv4 - Local Routes"), + .range("172.16.0.0/12" /* 255.240.0.0 */), + .range("192.168.0.0/16" /* 255.255.0.0 */), + ] +} diff --git a/Sources/NetworkProtection/Settings/TunnelSettings.swift b/Sources/NetworkProtection/Settings/VPNSettings.swift similarity index 72% rename from Sources/NetworkProtection/Settings/TunnelSettings.swift rename to Sources/NetworkProtection/Settings/VPNSettings.swift index 3aee2b58a..8eeed00b4 100644 --- a/Sources/NetworkProtection/Settings/TunnelSettings.swift +++ b/Sources/NetworkProtection/Settings/VPNSettings.swift @@ -1,5 +1,5 @@ // -// TunnelSettings.swift +// VPNSettings.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -19,22 +19,27 @@ import Combine import Foundation +// swiftlint:disable type_body_length + /// Persists and publishes changes to tunnel settings. /// -/// It's strongly recommended to use shared `UserDefaults` to initialize this class, as `TunnelSettingsUpdater` +/// It's strongly recommended to use shared `UserDefaults` to initialize this class, as `VPNSettings` /// can then detect settings changes using KVO even if they're applied by a different process or even by the user through /// the command line. /// -public final class TunnelSettings { +public final class VPNSettings { public enum Change: Codable { + case setConnectOnLogin(_ connectOnLogin: Bool) case setIncludeAllNetworks(_ includeAllNetworks: Bool) case setEnforceRoutes(_ enforceRoutes: Bool) case setExcludeLocalNetworks(_ excludeLocalNetworks: Bool) + case setNotifyStatusChanges(_ notifyStatusChanges: Bool) case setRegistrationKeyValidity(_ validity: RegistrationKeyValidity) case setSelectedServer(_ selectedServer: SelectedServer) case setSelectedLocation(_ selectedLocation: SelectedLocation) case setSelectedEnvironment(_ selectedEnvironment: SelectedEnvironment) + case setShowInMenuBar(_ showInMenuBar: Bool) } public enum RegistrationKeyValidity: Codable { @@ -86,6 +91,10 @@ public final class TunnelSettings { private(set) public lazy var changePublisher: AnyPublisher = { + let connectOnLoginPublisher = connectOnLoginPublisher.map { connectOnLogin in + Change.setConnectOnLogin(connectOnLogin) + }.eraseToAnyPublisher() + let includeAllNetworksPublisher = includeAllNetworksPublisher.map { includeAllNetworks in Change.setIncludeAllNetworks(includeAllNetworks) }.eraseToAnyPublisher() @@ -98,6 +107,10 @@ public final class TunnelSettings { Change.setExcludeLocalNetworks(excludeLocalNetworks) }.eraseToAnyPublisher() + let notifyStatusChangesPublisher = notifyStatusChangesPublisher.map { notifyStatusChanges in + Change.setNotifyStatusChanges(notifyStatusChanges) + }.eraseToAnyPublisher() + let registrationKeyValidityPublisher = registrationKeyValidityPublisher.map { validity in Change.setRegistrationKeyValidity(validity) }.eraseToAnyPublisher() @@ -114,13 +127,20 @@ public final class TunnelSettings { Change.setSelectedEnvironment(environment) }.eraseToAnyPublisher() + let showInMenuBarPublisher = showInMenuBarPublisher.map { showInMenuBar in + Change.setShowInMenuBar(showInMenuBar) + }.eraseToAnyPublisher() + return Publishers.MergeMany( + connectOnLoginPublisher, includeAllNetworksPublisher, enforceRoutesPublisher, excludeLocalNetworksPublisher, + notifyStatusChangesPublisher, serverChangePublisher, locationChangePublisher, - environmentChangePublisher).eraseToAnyPublisher() + environmentChangePublisher, + showInMenuBarPublisher).eraseToAnyPublisher() }() public init(defaults: UserDefaults) { @@ -130,24 +150,31 @@ public final class TunnelSettings { // MARK: - Resetting to Defaults public func resetToDefaults() { + defaults.resetNetworkProtectionSettingConnectOnLogin() defaults.resetNetworkProtectionSettingEnforceRoutes() defaults.resetNetworkProtectionSettingExcludeLocalNetworks() defaults.resetNetworkProtectionSettingIncludeAllNetworks() + defaults.resetNetworkProtectionSettingNotifyStatusChanges() defaults.resetNetworkProtectionSettingRegistrationKeyValidity() defaults.resetNetworkProtectionSettingSelectedServer() defaults.resetNetworkProtectionSettingSelectedEnvironment() + defaults.resetNetworkProtectionSettingShowInMenuBar() } // MARK: - Applying Changes public func apply(change: Change) { switch change { + case .setConnectOnLogin(let connectOnLogin): + self.connectOnLogin = connectOnLogin case .setEnforceRoutes(let enforceRoutes): self.enforceRoutes = enforceRoutes case .setExcludeLocalNetworks(let excludeLocalNetworks): self.excludeLocalNetworks = excludeLocalNetworks case .setIncludeAllNetworks(let includeAllNetworks): self.includeAllNetworks = includeAllNetworks + case .setNotifyStatusChanges(let notifyStatusChanges): + self.notifyStatusChanges = notifyStatusChanges case .setRegistrationKeyValidity(let registrationKeyValidity): self.registrationKeyValidity = registrationKeyValidity case .setSelectedServer(let selectedServer): @@ -156,6 +183,24 @@ public final class TunnelSettings { self.selectedLocation = selectedLocation case .setSelectedEnvironment(let selectedEnvironment): self.selectedEnvironment = selectedEnvironment + case .setShowInMenuBar(let showInMenuBar): + self.showInMenuBar = showInMenuBar + } + } + + // MARK: - Connect on Login + + public var connectOnLoginPublisher: AnyPublisher { + defaults.networkProtectionSettingConnectOnLoginPublisher + } + + public var connectOnLogin: Bool { + get { + defaults.networkProtectionSettingConnectOnLogin + } + + set { + defaults.networkProtectionSettingConnectOnLogin = newValue } } @@ -275,33 +320,61 @@ public final class TunnelSettings { } } + // MARK: - Show in Menu Bar + + public var showInMenuBarPublisher: AnyPublisher { + defaults.networkProtectionSettingShowInMenuBarPublisher + } + + public var showInMenuBar: Bool { + get { + defaults.networkProtectionSettingShowInMenuBar + } + + set { + defaults.networkProtectionSettingShowInMenuBar = newValue + } + } + + // MARK: - Notify Status Changes + + public var notifyStatusChangesPublisher: AnyPublisher { + defaults.networkProtectionNotifyStatusChangesPublisher + } + + public var notifyStatusChanges: Bool { + get { + defaults.networkProtectionNotifyStatusChanges + } + + set { + defaults.networkProtectionNotifyStatusChanges = newValue + } + } + // MARK: - Routes - public enum ExclusionListItem { - case section(String) - case exclusion(range: NetworkProtection.IPAddressRange, description: String? = nil, `default`: Bool) + public var excludedRoutes: [RoutingRange] { + var ipv4Ranges = RoutingRange.alwaysExcludedIPv4Ranges + + if excludeLocalNetworks { + ipv4Ranges += RoutingRange.localNetworkRanges + } + + return ipv4Ranges + RoutingRange.alwaysExcludedIPv6Ranges } - public let exclusionList: [ExclusionListItem] = [ - .section("IPv4 Local Routes"), - - .exclusion(range: "10.0.0.0/8" /* 255.0.0.0 */, description: "disabled for enforceRoutes", default: true), - .exclusion(range: "172.16.0.0/12" /* 255.240.0.0 */, default: true), - .exclusion(range: "192.168.0.0/16" /* 255.255.0.0 */, default: true), - .exclusion(range: "169.254.0.0/16" /* 255.255.0.0 */, description: "Link-local", default: true), - .exclusion(range: "127.0.0.0/8" /* 255.0.0.0 */, description: "Loopback", default: true), - .exclusion(range: "224.0.0.0/4" /* 240.0.0.0 (corrected subnet mask) */, description: "Multicast", default: true), - .exclusion(range: "100.64.0.0/16" /* 255.255.0.0 */, description: "Shared Address Space", default: true), - - .section("IPv6 Local Routes"), - .exclusion(range: "fe80::/10", description: "link local", default: false), - .exclusion(range: "ff00::/8", description: "multicast", default: false), - .exclusion(range: "fc00::/7", description: "local unicast", default: false), - .exclusion(range: "::1/128", description: "loopback", default: false), - - .section("duckduckgo.com"), - .exclusion(range: "52.142.124.215/32", default: false), - .exclusion(range: "52.250.42.157/32", default: false), - .exclusion(range: "40.114.177.156/32", default: false), - ] + public var excludedRanges: [IPAddressRange] { + excludedRoutes.compactMap { entry in + switch entry { + case .section: + // Nothing to map + return nil + case .range(let range, _): + return range + } + } + } } + +// swiftlint:enable type_body_length diff --git a/Sources/NetworkProtection/StartupOptions.swift b/Sources/NetworkProtection/StartupOptions.swift index 0adea7924..3989876d4 100644 --- a/Sources/NetworkProtection/StartupOptions.swift +++ b/Sources/NetworkProtection/StartupOptions.swift @@ -94,8 +94,8 @@ struct StartupOptions { let simulateCrash: Bool let simulateMemoryCrash: Bool let keyValidity: StoredOption - let selectedEnvironment: StoredOption - let selectedServer: StoredOption + let selectedEnvironment: StoredOption + let selectedServer: StoredOption let authToken: StoredOption let enableTester: StoredOption @@ -152,18 +152,18 @@ struct StartupOptions { } } - private static func readSelectedEnvironment(from options: [String: Any], resetIfNil: Bool) -> StoredOption { + private static func readSelectedEnvironment(from options: [String: Any], resetIfNil: Bool) -> StoredOption { StoredOption(resetIfNil: resetIfNil) { guard let environment = options[NetworkProtectionOptionKey.selectedEnvironment] as? String else { return nil } - return TunnelSettings.SelectedEnvironment(rawValue: environment) ?? .default + return VPNSettings.SelectedEnvironment(rawValue: environment) ?? .default } } - private static func readSelectedServer(from options: [String: Any], resetIfNil: Bool) -> StoredOption { + private static func readSelectedServer(from options: [String: Any], resetIfNil: Bool) -> StoredOption { StoredOption(resetIfNil: resetIfNil) { guard let serverName = options[NetworkProtectionOptionKey.selectedServer] as? String else { diff --git a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift index 4e3bff243..8a1b06050 100644 --- a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift +++ b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift @@ -20,46 +20,41 @@ import Foundation final public class NetworkProtectionNotificationsPresenterTogglableDecorator: NetworkProtectionNotificationsPresenter { - private let notificationSettingsStore: NetworkProtectionNotificationsSettingsStore + private let settings: VPNSettings private let wrappeePresenter: NetworkProtectionNotificationsPresenter - public init(notificationSettingsStore: NetworkProtectionNotificationsSettingsStore, wrappee: NetworkProtectionNotificationsPresenter) { - self.notificationSettingsStore = notificationSettingsStore + public init(settings: VPNSettings, wrappee: NetworkProtectionNotificationsPresenter) { + self.settings = settings self.wrappeePresenter = wrappee } public func showConnectedNotification(serverLocation: String?) { - guard notificationSettingsStore.alertsEnabled else { - return + if settings.notifyStatusChanges { + wrappeePresenter.showConnectedNotification(serverLocation: serverLocation) } - wrappeePresenter.showConnectedNotification(serverLocation: serverLocation) } - + public func showReconnectingNotification() { - guard notificationSettingsStore.alertsEnabled else { - return + if settings.notifyStatusChanges { + wrappeePresenter.showReconnectingNotification() } - wrappeePresenter.showReconnectingNotification() } - + public func showConnectionFailureNotification() { - guard notificationSettingsStore.alertsEnabled else { - return + if settings.notifyStatusChanges { + wrappeePresenter.showConnectionFailureNotification() } - wrappeePresenter.showConnectionFailureNotification() } - + public func showSupersededNotification() { - guard notificationSettingsStore.alertsEnabled else { - return + if settings.notifyStatusChanges { + wrappeePresenter.showSupersededNotification() } - wrappeePresenter.showSupersededNotification() } - + public func showTestNotification() { - guard notificationSettingsStore.alertsEnabled else { - return + if settings.notifyStatusChanges { + wrappeePresenter.showTestNotification() } - wrappeePresenter.showTestNotification() } } diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionNotificationsSettingsStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionNotificationsSettingsStore.swift deleted file mode 100644 index 2e4384fed..000000000 --- a/Sources/NetworkProtection/Storage/NetworkProtectionNotificationsSettingsStore.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// NetworkProtectionNotificationsSettingsStore.swift -// DuckDuckGo -// -// 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 - -public protocol NetworkProtectionNotificationsSettingsStore { - var alertsEnabled: Bool { get set } -} - -final public class NetworkProtectionNotificationsSettingsUserDefaultsStore: NetworkProtectionNotificationsSettingsStore { - private enum Key { - static let alerts = "com.duckduckgo.vpnNotificationSettings.alertsEnabled" - } - - private let userDefaults: UserDefaults - - public init(userDefaults: UserDefaults) { - self.userDefaults = userDefaults - } - - public var alertsEnabled: Bool { - get { - guard self.userDefaults.object(forKey: Key.alerts) != nil else { - return true - } - return self.userDefaults.bool(forKey: Key.alerts) - } - set { - self.userDefaults.set(newValue, forKey: Key.alerts) - } - } -} diff --git a/Sources/NetworkProtectionTestUtils/Storage/MockNetworkProtectionNotificationsSettingsStore.swift b/Sources/NetworkProtectionTestUtils/Storage/MockNetworkProtectionNotificationsSettingsStore.swift deleted file mode 100644 index dcaf3adf3..000000000 --- a/Sources/NetworkProtectionTestUtils/Storage/MockNetworkProtectionNotificationsSettingsStore.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// MockNetworkProtectionNotificationsSettingsStore.swift -// DuckDuckGo -// -// 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 NetworkProtection - -public class MockNetworkProtectionNotificationsSettingsStore: NetworkProtectionNotificationsSettingsStore { - public var alertsEnabled: Bool = false -} diff --git a/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift b/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift index 32a8d8c44..abd7973d5 100644 --- a/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift +++ b/Tests/NetworkProtectionTests/NetworkProtectionClientTests.swift @@ -21,13 +21,13 @@ import XCTest final class NetworkProtectionClientTests: XCTestCase { var testDefaults: UserDefaults! - var tunnelSettings: TunnelSettings! + var settings: VPNSettings! var client: NetworkProtectionBackendClient! override func setUp() { super.setUp() testDefaults = UserDefaults(suiteName: "com.duckduckgo.browserserviceskit.tests.\(String(describing: type(of: self)))")! - tunnelSettings = TunnelSettings(defaults: testDefaults) + settings = VPNSettings(defaults: testDefaults) client = NetworkProtectionBackendClient(environment: .default) } From 1400c9ca17dd770d6eb708288f97c9aab749d0ef Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Tue, 28 Nov 2023 22:25:21 -0500 Subject: [PATCH 05/39] Update privacy defaults in BSK (#569) * Update Class E IP address class to 240.0.0.0-255.255.255.255 (used to be 240.0.0.0-240.255.255.255) * Use DNS server IP from /register response * Remove DDG exclusion --------- Co-authored-by: Diego Rey Mendez --- .../Models/NetworkProtectionServerInfo.swift | 2 ++ .../NetworkProtectionDeviceManager.swift | 8 ++++---- Sources/NetworkProtection/PacketTunnelProvider.swift | 1 + Sources/NetworkProtection/Settings/RoutingRange.swift | 9 ++------- .../Mocks/NetworkProtectionServerMocks.swift | 6 +++++- .../NetworkProtectionServerInfoTests.swift | 2 ++ .../Resources/servers-original-endpoint.json | 2 +- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Sources/NetworkProtection/Models/NetworkProtectionServerInfo.swift b/Sources/NetworkProtection/Models/NetworkProtectionServerInfo.swift index 60833ddfc..b0f0776eb 100644 --- a/Sources/NetworkProtection/Models/NetworkProtectionServerInfo.swift +++ b/Sources/NetworkProtection/Models/NetworkProtectionServerInfo.swift @@ -43,6 +43,7 @@ public struct NetworkProtectionServerInfo: Codable, Equatable, Sendable { public let publicKey: String public let hostNames: [String] public let ips: [AnyIPAddress] + public let internalIP: AnyIPAddress public let port: UInt16 public let attributes: ServerAttributes @@ -51,6 +52,7 @@ public struct NetworkProtectionServerInfo: Codable, Equatable, Sendable { case publicKey case hostNames = "hostnames" case ips + case internalIP = "internalIp" case port case attributes } diff --git a/Sources/NetworkProtection/NetworkProtectionDeviceManager.swift b/Sources/NetworkProtection/NetworkProtectionDeviceManager.swift index 34482ac2f..07159f947 100644 --- a/Sources/NetworkProtection/NetworkProtectionDeviceManager.swift +++ b/Sources/NetworkProtection/NetworkProtectionDeviceManager.swift @@ -277,6 +277,7 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement { addressRange: interfaceAddressRange, includedRoutes: includedRoutes, excludedRoutes: excludedRoutes, + dns: [DNSServer(address: server.serverInfo.internalIP)], isKillSwitchEnabled: isKillSwitchEnabled) return TunnelConfiguration(name: "Network Protection", interface: interface, peers: [peerConfiguration]) @@ -291,15 +292,13 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement { return peerConfiguration } + // swiftlint:disable function_parameter_count func interfaceConfiguration(privateKey: PrivateKey, addressRange: IPAddressRange, includedRoutes: [IPAddressRange], excludedRoutes: [IPAddressRange], + dns: [DNSServer], isKillSwitchEnabled: Bool) -> InterfaceConfiguration { - // TO BE moved out to config - let dns = [ - DNSServer(from: "10.11.12.1")! - ] var includedRoutes = includedRoutes // Tunnel doesn‘t work with ‘enforceRoutes‘ option when DNS IP/addressRange is in includedRoutes if !isKillSwitchEnabled { @@ -313,6 +312,7 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement { listenPort: 51821, dns: dns) } + // swiftlint:enable function_parameter_count private func handle(clientError: NetworkProtectionClientError) { if case .invalidAuthToken = clientError { diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 04cbcb202..d40b104c2 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -711,6 +711,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { log: .networkProtection, selectedServerInfo.serverLocation, selectedServerInfo.name) + os_log("🔵 Excluded routes: %{public}@", log: .networkProtection, type: .info, String(describing: excludedRoutes)) let tunnelConfiguration = configurationResult.0 diff --git a/Sources/NetworkProtection/Settings/RoutingRange.swift b/Sources/NetworkProtection/Settings/RoutingRange.swift index 2b5582a37..04426f9a2 100644 --- a/Sources/NetworkProtection/Settings/RoutingRange.swift +++ b/Sources/NetworkProtection/Settings/RoutingRange.swift @@ -29,16 +29,11 @@ public enum RoutingRange { .range("127.0.0.0/8" /* 255.0.0.0 */, description: "Loopback"), .range("169.254.0.0/16" /* 255.255.0.0 */, description: "Link-local"), .range("224.0.0.0/4" /* 240.0.0.0 */, description: "Multicast"), - .range("240.0.0.0/8" /* 255.0.0.0 */, description: "Multicast"), - - .section("duckduckgo.com"), - .range("52.142.124.215/32"), - .range("52.250.42.157/32"), - .range("40.114.177.156/32"), + .range("240.0.0.0/4" /* 240.0.0.0 */, description: "Class E"), ] public static let alwaysExcludedIPv6Ranges: [RoutingRange] = [ - // When need to figure out what will happen to these when + // We need to figure out what will happen to these when // excludeLocalNetworks is OFF. // For now though, I'm keeping these but leaving these always excluded // as IPv6 is out of scope. diff --git a/Tests/NetworkProtectionTests/Mocks/NetworkProtectionServerMocks.swift b/Tests/NetworkProtectionTests/Mocks/NetworkProtectionServerMocks.swift index f502997fe..4971045f7 100644 --- a/Tests/NetworkProtectionTests/Mocks/NetworkProtectionServerMocks.swift +++ b/Tests/NetworkProtectionTests/Mocks/NetworkProtectionServerMocks.swift @@ -32,7 +32,8 @@ extension NetworkProtectionServerInfo { static let mock = NetworkProtectionServerInfo(name: "Mock Server", publicKey: "ovn9RpzUuvQ4XLQt6B3RKuEXGIxa5QpTnehjduZlcSE=", hostNames: ["duckduckgo.com"], - ips: ["192.168.1.1"], + ips: ["192.168.1.1"], + internalIP: "10.11.12.1", port: 443, attributes: .init(city: "City", country: "Country", state: "State", timezoneOffset: 0)) @@ -40,6 +41,7 @@ extension NetworkProtectionServerInfo { publicKey: "ovn9RpzUuvQ4XLQt6B3RKuEXGIxa5QpTnehjduZlcSE=", hostNames: ["duckduckgo.com"], ips: [], + internalIP: "10.11.12.1", port: 443, attributes: .init(city: "City", country: "Country", state: "State", timezoneOffset: 0)) @@ -47,6 +49,7 @@ extension NetworkProtectionServerInfo { publicKey: "ovn9RpzUuvQ4XLQt6B3RKuEXGIxa5QpTnehjduZlcSE=", hostNames: [], ips: ["192.168.1.1"], + internalIP: "10.11.12.1", port: 443, attributes: .init(city: "City", country: "Country", state: "State", timezoneOffset: 0)) @@ -55,6 +58,7 @@ extension NetworkProtectionServerInfo { publicKey: publicKey, hostNames: ["duckduckgo.com"], ips: ["192.168.1.1"], + internalIP: "10.11.12.1", port: 443, attributes: .init(city: "City", country: "Country", state: "State", timezoneOffset: 0)) } diff --git a/Tests/NetworkProtectionTests/NetworkProtectionServerInfoTests.swift b/Tests/NetworkProtectionTests/NetworkProtectionServerInfoTests.swift index 97e2596ad..3d4a8da2c 100644 --- a/Tests/NetworkProtectionTests/NetworkProtectionServerInfoTests.swift +++ b/Tests/NetworkProtectionTests/NetworkProtectionServerInfoTests.swift @@ -27,6 +27,7 @@ final class NetworkProtectionServerInfoTests: XCTestCase { publicKey: "", hostNames: [], ips: [], + internalIP: "10.11.12.1", port: 42, attributes: .init(city: "Amsterdam", country: "nl", state: "na", timezoneOffset: 3600)) @@ -38,6 +39,7 @@ final class NetworkProtectionServerInfoTests: XCTestCase { publicKey: "", hostNames: [], ips: [], + internalIP: "10.11.12.1", port: 42, attributes: .init(city: "New York", country: "us", state: "ny", timezoneOffset: 3600)) diff --git a/Tests/NetworkProtectionTests/Resources/servers-original-endpoint.json b/Tests/NetworkProtectionTests/Resources/servers-original-endpoint.json index c5ba4eb55..a3169013a 100644 --- a/Tests/NetworkProtectionTests/Resources/servers-original-endpoint.json +++ b/Tests/NetworkProtectionTests/Resources/servers-original-endpoint.json @@ -1 +1 @@ -[{"registeredAt":"2023-02-03T17:42:36.263760127-05:00","server":{"name":"egress.usw.1","attributes":{"city":"El Segundo","country":"us","latitude":33.9192,"longitude":-118.4165,"region":"North America","state":"ca","tzOffset":-28800},"publicKey":"R/BMR6Rr5rzvp7vSIWdAtgAmOLK9m7CqTcDynblM3Us=","hostnames":[],"ips":["162.245.204.100"],"port":443}},{"registeredAt":"2023-02-03T17:42:36.613040955-05:00","server":{"name":"egress.euw.1","attributes":{"city":"Rotterdam","country":"nl","latitude":51.9225,"longitude":4.4792,"region":"Europe","state":"na","tzOffset":3600},"publicKey":"ocUfgaqaN/s/D3gTwJstipGh03T2v6wLL+aVtg3Viz4=","hostnames":[],"ips":["31.204.129.36"],"port":443}},{"registeredAt":"2023-02-03T17:42:34.946832238-05:00","server":{"name":"egress.euw.2","attributes":{"city":"Rotterdam","country":"nl","latitude":51.9225,"longitude":4.4792,"region":"Europe","state":"na","tzOffset":3600},"publicKey":"4PnM/V0CodegK44rd9fKTxxS9QDVTw13j8fxKsVud3s=","hostnames":[],"ips":["31.204.129.39"],"port":443}},{"registeredAt":"2023-02-03T17:42:35.130289666-05:00","server":{"name":"egress.use.1","attributes":{"city":"Newark","country":"us","latitude":40.7357,"longitude":-74.1724,"region":"North America","state":"nj","tzOffset":-18000},"publicKey":"L4gDTg3KqbhjjiN99n/Zmwxwmbv+P+n8ZZVL0v34cAs=","hostnames":[],"ips":["109.200.208.196"],"port":443}},{"registeredAt":"2023-02-03T17:42:35.913046706-05:00","server":{"name":"egress.use.2","attributes":{"city":"Newark","country":"us","latitude":40.7357,"longitude":-74.1724,"region":"North America","state":"nj","tzOffset":-18000},"publicKey":"q3YJJUwMNP31J8qSvMdVsxASKNcjrm8ep8cLcI0qViY=","hostnames":[],"ips":["109.200.208.198"],"port":443}},{"registeredAt":"2023-02-03T17:42:35.661113901-05:00","server":{"name":"egress.usw.2","attributes":{"city":"El Segundo","country":"us","latitude":33.9192,"longitude":-118.4165,"region":"North America","state":"ca","tzOffset":-28800},"publicKey":"8JjNmnFYZA+CnWAkbiucDrUJ70wl+Tl3O3ETkRgw028=","hostnames":[],"ips":["162.245.204.102"],"port":443}}] +[{"registeredAt":"2023-02-03T17:42:36.263760127-05:00","server":{"name":"egress.usw.1","attributes":{"city":"El Segundo","country":"us","latitude":33.9192,"longitude":-118.4165,"region":"North America","state":"ca","tzOffset":-28800},"publicKey":"R/BMR6Rr5rzvp7vSIWdAtgAmOLK9m7CqTcDynblM3Us=","hostnames":[],"ips":["162.245.204.100"],"internalIp":"10.11.12.1","port":443}},{"registeredAt":"2023-02-03T17:42:36.613040955-05:00","server":{"name":"egress.euw.1","attributes":{"city":"Rotterdam","country":"nl","latitude":51.9225,"longitude":4.4792,"region":"Europe","state":"na","tzOffset":3600},"publicKey":"ocUfgaqaN/s/D3gTwJstipGh03T2v6wLL+aVtg3Viz4=","hostnames":[],"ips":["31.204.129.36"],"internalIp":"10.11.12.1","port":443}},{"registeredAt":"2023-02-03T17:42:34.946832238-05:00","server":{"name":"egress.euw.2","attributes":{"city":"Rotterdam","country":"nl","latitude":51.9225,"longitude":4.4792,"region":"Europe","state":"na","tzOffset":3600},"publicKey":"4PnM/V0CodegK44rd9fKTxxS9QDVTw13j8fxKsVud3s=","hostnames":[],"ips":["31.204.129.39"],"internalIp":"10.11.12.1","port":443}},{"registeredAt":"2023-02-03T17:42:35.130289666-05:00","server":{"name":"egress.use.1","attributes":{"city":"Newark","country":"us","latitude":40.7357,"longitude":-74.1724,"region":"North America","state":"nj","tzOffset":-18000},"publicKey":"L4gDTg3KqbhjjiN99n/Zmwxwmbv+P+n8ZZVL0v34cAs=","hostnames":[],"ips":["109.200.208.196"],"internalIp":"10.11.12.1","port":443}},{"registeredAt":"2023-02-03T17:42:35.913046706-05:00","server":{"name":"egress.use.2","attributes":{"city":"Newark","country":"us","latitude":40.7357,"longitude":-74.1724,"region":"North America","state":"nj","tzOffset":-18000},"publicKey":"q3YJJUwMNP31J8qSvMdVsxASKNcjrm8ep8cLcI0qViY=","hostnames":[],"ips":["109.200.208.198"],"internalIp":"10.11.12.1","port":443}},{"registeredAt":"2023-02-03T17:42:35.661113901-05:00","server":{"name":"egress.usw.2","attributes":{"city":"El Segundo","country":"us","latitude":33.9192,"longitude":-118.4165,"region":"North America","state":"ca","tzOffset":-28800},"publicKey":"8JjNmnFYZA+CnWAkbiucDrUJ70wl+Tl3O3ETkRgw028=","hostnames":[],"ips":["162.245.204.102"],"internalIp":"10.11.12.1","port":443}}] From f1ae021c12c2afe3ade7ec0e41712725c03c3da4 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 30 Nov 2023 08:19:55 +0100 Subject: [PATCH 06/39] Allow automated fetching of synced bookmarks' favicons (#564) Task/Issue URL: https://app.asana.com/0/0/1205949780297088/f Tech Design URL: https://app.asana.com/0/481882893211075/1204986998781220/f Description: Add BookmarksFaviconFetcher that is used to fetch favicons for bookmarks received by Sync. Fetcher is opt-in, controlled by a setting inside Sync settings (with an additional in-context onboarding popup presented from client apps). Fetcher uses LinkPresentation framework to obtain a favicon for a given domain, and in case of failure it falls back to checking hardcoded favicon URLs. Fetcher keeps a state internally, by saving list of bookmarks IDs that need processing to a file on disk. Fetcher plugs into clients' implementation of favicon storage by exposing FaviconStoring protocol. Fetcher performs fetching on a serial operation queue. Each fetcher invocation cancels previously scheduled operation and schedules a new one. Updating fetcher state is also scheduled on the operation queue - state updates don't support cancelling and always finish before next operation is started. --- Package.swift | 2 +- Sources/Bookmarks/BookmarkUtils.swift | 12 + .../BookmarksFaviconsFetcher.swift | 251 ++++++++++++ .../BookmarksFaviconsFetcherStateStore.swift | 62 +++ .../FaviconsFetcher/FaviconFetcher.swift | 107 +++++ .../FaviconsFetchOperation.swift | 376 ++++++++++++++++++ Sources/DDGSync/DataProvider.swift | 40 +- .../Bookmarks/BookmarksProvider.swift | 25 +- .../internal/BookmarksResponseHandler.swift | 27 +- .../Credentials/CredentialsProvider.swift | 1 + .../Settings/SettingsProvider.swift | 1 + .../BookmarkDomainsTests.swift | 181 +++++++++ .../BookmarksFaviconsFetcherTests.swift | 299 ++++++++++++++ .../FaviconsFetchOperationTests.swift | 311 +++++++++++++++ .../FaviconsFetcherMocks.swift | 64 +++ .../Bookmarks/BookmarksProviderTests.swift | 37 ++ .../helpers/BookmarksProviderTestsBase.swift | 9 +- 17 files changed, 1787 insertions(+), 18 deletions(-) create mode 100644 Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcher.swift create mode 100644 Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcherStateStore.swift create mode 100644 Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift create mode 100644 Sources/Bookmarks/FaviconsFetcher/FaviconsFetchOperation.swift create mode 100644 Tests/BookmarksTests/FaviconsFetcher/BookmarkDomainsTests.swift create mode 100644 Tests/BookmarksTests/FaviconsFetcher/BookmarksFaviconsFetcherTests.swift create mode 100644 Tests/BookmarksTests/FaviconsFetcher/FaviconsFetchOperationTests.swift create mode 100644 Tests/BookmarksTests/FaviconsFetcher/FaviconsFetcherMocks.swift diff --git a/Package.swift b/Package.swift index a148a6747..cbd82bd10 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( name: "BrowserServicesKit", platforms: [ .iOS("14.0"), - .macOS("10.15") + .macOS("11.4") ], products: [ // Exported libraries diff --git a/Sources/Bookmarks/BookmarkUtils.swift b/Sources/Bookmarks/BookmarkUtils.swift index cad0920b7..d08a6c671 100644 --- a/Sources/Bookmarks/BookmarkUtils.swift +++ b/Sources/Bookmarks/BookmarkUtils.swift @@ -115,6 +115,18 @@ public struct BookmarkUtils { } } + public static func fetchAllBookmarksUUIDs(in context: NSManagedObjectContext) -> [String] { + let request = NSFetchRequest(entityName: "BookmarkEntity") + request.predicate = NSPredicate(format: "%K == NO AND %K == NO", + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion)) + request.resultType = .dictionaryResultType + request.propertiesToFetch = [#keyPath(BookmarkEntity.uuid)] + + let result = (try? context.fetch(request) as? [Dictionary]) ?? [] + return result.compactMap { $0[#keyPath(BookmarkEntity.uuid)] as? String } + } + public static func fetchBookmark(for url: URL, predicate: NSPredicate = NSPredicate(value: true), context: NSManagedObjectContext) -> BookmarkEntity? { diff --git a/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcher.swift b/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcher.swift new file mode 100644 index 000000000..ee02273eb --- /dev/null +++ b/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcher.swift @@ -0,0 +1,251 @@ +// +// BookmarksFaviconsFetcher.swift +// DuckDuckGo +// +// 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 Combine +import Common +import CoreData +import Foundation +import Persistence + +/** + * This protocol abstracts favicons fetcher state storing interface. + */ +public protocol BookmarksFaviconsFetcherStateStoring: AnyObject { + func getBookmarkIDs() throws -> Set + func storeBookmarkIDs(_ ids: Set) throws +} + +/** + * This protocol abstracts a mechanism of fetching a single favicon + */ +public protocol FaviconFetching { + /** + * Fetch a favicon for a document specified by `url`. + * + * Returns optional favicon image data and an optional + * favicon URL (if the fetcher is able to provide it). + */ + func fetchFavicon(for url: URL) async throws -> (Data?, URL?) +} + +/** + * This protocol abstracts favicons storing interface provided by client apps. + */ +public protocol FaviconStoring { + /** + * Returns a boolean value telling whether the store has a cached favicon for a given `domain`. + */ + func hasFavicon(for domain: String) -> Bool + + /** + * Stores favicon with `imageData` for document specified by `documentURL`. + * Optional `url` parameter, if provided, specifies the URL of the favicon. + */ + func storeFavicon(_ imageData: Data, with url: URL?, for documentURL: URL) async throws +} + +/** + * Errors that may be reported by `BookmarksFaviconsFetcher`. + */ +public enum BookmarksFaviconsFetcherError: CustomNSError { + case failedToStoreBookmarkIDs(Error) + case failedToRetrieveBookmarkIDs(Error) + case other(Error) + + public static let errorDomain: String = "BookmarksFaviconsFetcherError" + + public var errorCode: Int { + switch self { + case .failedToStoreBookmarkIDs: + return 1 + case .failedToRetrieveBookmarkIDs: + return 2 + case .other: + return 255 + } + } + + public var underlyingError: Error { + switch self { + case .failedToStoreBookmarkIDs(let error), .failedToRetrieveBookmarkIDs(let error), .other(let error): + return error + } + } +} + +/** + * This class manages fetching favicons for bookmarks updated by Sync. + * + * It takes modified and deleted bookmark IDs as input, fetches bookmarks' URLs, + * extracts their domains and fetches favicons for those domains that don't have a favicon cached. + */ +public final class BookmarksFaviconsFetcher { + + @Published public private(set) var isFetchingInProgress: Bool = false + public let fetchingDidFinishPublisher: AnyPublisher, Never> + + public init( + database: CoreDataDatabase, + stateStore: BookmarksFaviconsFetcherStateStoring, + fetcher: FaviconFetching, + faviconStore: FaviconStoring, + errorEvents: EventMapping?, + log: @escaping @autoclosure () -> OSLog = .disabled + ) { + self.database = database + self.stateStore = stateStore + self.fetcher = fetcher + self.faviconStore = faviconStore + self.errorEvents = errorEvents + self.getLog = log + + fetchingDidFinishPublisher = fetchingDidFinishSubject.eraseToAnyPublisher() + + isFetchingInProgressCancellable = Publishers + .Merge(fetchingDidStartSubject.map({ true }), fetchingDidFinishSubject.map({ _ in false })) + .prepend(false) + .removeDuplicates() + .assign(to: \.isFetchingInProgress, onWeaklyHeld: self) + } + + /** + * This function should be called right after favicons fetching was turned on. + * + * This function cancels any pending fetch operation prior to updating fetcher state. + * + * It sets up initial state by fetching all bookmarks' IDs. + * After this function is called, `startFetching` can be called to go through + * all bookmarks in the database and process those without a favicon. + */ + public func initializeFetcherState() { + cancelOngoingFetchingIfNeeded() + operationQueue.addOperation { + do { + let allBookmarkIDs = self.fetchAllBookmarksUUIDs() + try self.stateStore.storeBookmarkIDs(allBookmarkIDs) + } catch { + os_log(.debug, log: self.log, "Error updating bookmark IDs: %{public}s", error.localizedDescription) + if let fetcherError = error as? BookmarksFaviconsFetcherError { + self.errorEvents?.fire(fetcherError) + } else { + self.errorEvents?.fire(.other(error)) + } + } + } + } + + /** + * This function should be called whenever sync receives new data. + * + * It is only responsible for updating the fetcher state. Actual fetching + * needs `startFetching` to be called after calling this function. + * + * This function cancels any pending fetch operation prior to updating fetcher state. + * + * - Parameter modified: IDs of bookmarks that have been modified by Sync. + * - Parameter deleted: IDs of bookmarks that have been deleted by Sync. + */ + public func updateBookmarkIDs(modified: Set, deleted: Set) { + cancelOngoingFetchingIfNeeded() + operationQueue.addOperation { + do { + let ids = try self.stateStore.getBookmarkIDs().union(modified).subtracting(deleted) + try self.stateStore.storeBookmarkIDs(ids) + } catch { + os_log(.debug, log: self.log, "Error updating bookmark IDs: %{public}s", error.localizedDescription) + if let fetcherError = error as? BookmarksFaviconsFetcherError { + self.errorEvents?.fire(fetcherError) + } else { + self.errorEvents?.fire(.other(error)) + } + } + } + } + + /** + * Starts favicons fetch operation. + * + * This function cancels any pending fetch operation and schedules a new operation. + */ + public func startFetching() { + cancelOngoingFetchingIfNeeded() + let operation = FaviconsFetchOperation( + database: database, + stateStore: stateStore, + fetcher: fetcher, + faviconStore: faviconStore, + log: self.log + ) + operation.didStart = { [weak self] in + self?.fetchingDidStartSubject.send() + } + operation.didFinish = { [weak self] error in + if let error { + self?.fetchingDidFinishSubject.send(.failure(error)) + if let fetcherError = error as? BookmarksFaviconsFetcherError { + self?.errorEvents?.fire(fetcherError) + } else { + self?.errorEvents?.fire(.other(error)) + } + } else { + self?.fetchingDidFinishSubject.send(.success(())) + } + } + operationQueue.addOperation(operation) + } + + /** + * Cancels any favicons fetching operations that may be in progress or scheduled for running. + */ + public func cancelOngoingFetchingIfNeeded() { + operationQueue.cancelAllOperations() + } + + let operationQueue: OperationQueue = { + let queue = OperationQueue() + queue.name = "com.duckduckgo.sync.bookmarksFaviconsFetcher" + queue.qualityOfService = .userInitiated + queue.maxConcurrentOperationCount = 1 + return queue + }() + + private func fetchAllBookmarksUUIDs() -> Set { + let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) + var ids = [String]() + context.performAndWait { + ids = BookmarkUtils.fetchAllBookmarksUUIDs(in: context) + } + return Set(ids) + } + + private let errorEvents: EventMapping? + private let database: CoreDataDatabase + private let stateStore: BookmarksFaviconsFetcherStateStoring + private let fetcher: FaviconFetching + private let faviconStore: FaviconStoring + + private var isFetchingInProgressCancellable: AnyCancellable? + private let fetchingDidStartSubject = PassthroughSubject() + private let fetchingDidFinishSubject = PassthroughSubject, Never>() + + private var log: OSLog { + getLog() + } + private let getLog: () -> OSLog +} diff --git a/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcherStateStore.swift b/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcherStateStore.swift new file mode 100644 index 000000000..284ac19a7 --- /dev/null +++ b/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcherStateStore.swift @@ -0,0 +1,62 @@ +// +// BookmarksFaviconsFetcherStateStore.swift +// DuckDuckGo +// +// 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 + +public class BookmarksFaviconsFetcherStateStore: BookmarksFaviconsFetcherStateStoring { + + let dataDirectoryURL: URL + let missingIDsFileURL: URL + + public init(applicationSupportURL: URL) throws { + dataDirectoryURL = applicationSupportURL.appendingPathComponent("FaviconsFetcher") + missingIDsFileURL = dataDirectoryURL.appendingPathComponent("missingIDs") + + try initStorage() + } + + private func initStorage() throws { + if !FileManager.default.fileExists(atPath: dataDirectoryURL.path) { + try FileManager.default.createDirectory(at: dataDirectoryURL, withIntermediateDirectories: true) + } + if !FileManager.default.fileExists(atPath: missingIDsFileURL.path) { + FileManager.default.createFile(atPath: missingIDsFileURL.path, contents: Data()) + } + } + + public func getBookmarkIDs() throws -> Set { + do { + let data = try Data(contentsOf: missingIDsFileURL) + guard let rawValue = String(data: data, encoding: .utf8) else { + return [] + } + return Set(rawValue.components(separatedBy: ",")) + } catch { + throw BookmarksFaviconsFetcherError.failedToRetrieveBookmarkIDs(error) + } + } + + public func storeBookmarkIDs(_ ids: Set) throws { + do { + try ids.joined(separator: ",").data(using: .utf8)?.write(to: missingIDsFileURL) + } catch { + throw BookmarksFaviconsFetcherError.failedToStoreBookmarkIDs(error) + } + } +} diff --git a/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift b/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift new file mode 100644 index 000000000..bd2a96d73 --- /dev/null +++ b/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift @@ -0,0 +1,107 @@ +// +// FaviconFetcher.swift +// DuckDuckGo +// +// 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 LinkPresentation +import UniformTypeIdentifiers + +public final class FaviconFetcher: NSObject, FaviconFetching { + + public override init() {} + + public func fetchFavicon(for url: URL) async throws -> (Data?, URL?) { + /// DuckDuckGo Privacy Browser uses built-in functionality from Apple to fetch the highest quality favicons for bookmarks and favorites. + /// This functionality uses a user agent that is different from other network requests made by the apps in order to find the best favicon available. + let metadataFetcher = LPMetadataProvider() + + // Allow LinkPresentation to fail so that we can fall back to fetching hardcoded paths + let metadata: LPLinkMetadata? = await { + if #available(iOS 15.0, macOS 12.0, *) { + var request = URLRequest(url: url) + request.attribution = .user + return try? await metadataFetcher.startFetchingMetadata(for: request) + } else { + return try? await metadataFetcher.startFetchingMetadata(for: url) + } + }() + + // If LinkPresentation returned metadata, try retrieving favicon data + let imageData: Data? = await withCheckedContinuation { continuation in + guard let iconProvider = metadata?.iconProvider else { + continuation.resume(returning: nil) + return + } + iconProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { data, _ in + guard let data = data as? Data else { + continuation.resume(returning: nil) + return + } + continuation.resume(returning: data) + } + } + + guard let imageData else { + return try await lookUpHardcodedFaviconPath(for: url) + } + + return (imageData, nil) + } + + private func lookUpHardcodedFaviconPath(for url: URL) async throws -> (Data?, URL?) { + guard let host = url.host else { + return (nil, nil) + } + + var faviconImageData: Data? + var faviconURL: URL? + + for path in Const.hardcodedFaviconPaths { + let potentialFaviconURL = URL(string: "\(URL.NavigationalScheme.https.separated())\(host)/\(path)") + guard let potentialFaviconURL else { + continue + } + let (data, response) = try await faviconsURLSession.data(from: potentialFaviconURL) + if (response as? HTTPURLResponse)?.statusCode == 200 { + faviconImageData = data + faviconURL = potentialFaviconURL + break + } + } + + return (faviconImageData, faviconURL) + } + + enum Const { + static let hardcodedFaviconPaths = ["apple-touch-icon.png", "favicon.ico"] + } + + private(set) lazy var faviconsURLSession = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: nil) +} + +extension FaviconFetcher: URLSessionTaskDelegate { + + public func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest + ) async -> URLRequest? { + return request + } +} diff --git a/Sources/Bookmarks/FaviconsFetcher/FaviconsFetchOperation.swift b/Sources/Bookmarks/FaviconsFetcher/FaviconsFetchOperation.swift new file mode 100644 index 000000000..8f21a44ac --- /dev/null +++ b/Sources/Bookmarks/FaviconsFetcher/FaviconsFetchOperation.swift @@ -0,0 +1,376 @@ +// +// FaviconsFetchOperation.swift +// DuckDuckGo +// +// 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 Combine +import Common +import CoreData +import Persistence + +final class FaviconsFetchOperation: Operation { + + enum FaviconFetchError: Error { + case connectionError + case requestError + } + + enum Const { + static let maximumConcurrentFetches = 10 + } + + var didStart: (() -> Void)? { + get { + lock.lock() + defer { lock.unlock() } + return _didStart + } + set { + lock.lock() + defer { lock.unlock() } + _didStart = newValue + } + } + + var didFinish: ((Error?) -> Void)? { + get { + lock.lock() + defer { lock.unlock() } + return _didFinish + } + set { + lock.lock() + defer { lock.unlock() } + _didFinish = newValue + } + } + + init( + database: CoreDataDatabase, + stateStore: BookmarksFaviconsFetcherStateStoring, + fetcher: FaviconFetching, + faviconStore: FaviconStoring, + log: @escaping @autoclosure () -> OSLog = .disabled + ) { + self.database = database + self.stateStore = stateStore + self.fetcher = fetcher + self.faviconStore = faviconStore + self.getLog = log + } + + override func start() { + guard !isCancelled else { + isExecuting = false + isFinished = true + return + } + + isExecuting = true + isFinished = false + + didStart?() + + Task { + defer { + isExecuting = false + isFinished = true + } + + do { + try await fetchFavicons() + didFinish?(nil) + } catch { + didFinish?(error) + } + } + } + + func fetchFavicons() async throws { + var idsToProcess = try stateStore.getBookmarkIDs() + + guard !idsToProcess.isEmpty else { + os_log(.debug, log: log, "No new Favicons to fetch") + return + } + + os_log(.debug, log: log, "Favicons Fetch Operation started") + defer { + os_log(.debug, log: log, "Favicons Fetch Operation finished") + } + + var bookmarkDomains = mapBookmarkDomainsToUUIDs(for: idsToProcess) + bookmarkDomains.filterDomains { [weak self] domain in + self?.faviconStore.hasFavicon(for: domain) == false + } + + idsToProcess = bookmarkDomains.allUUIDs + + try checkCancellation() + + var allDomains = bookmarkDomains.allDomains + + guard !allDomains.isEmpty else { + os_log(.debug, log: log, "No favicons to fetch") + try stateStore.storeBookmarkIDs(idsToProcess) + return + } + os_log(.debug, log: log, "Will try to fetch favicons for %{public}d domains", allDomains.count) + + while !allDomains.isEmpty { + let numberOfDomainsToFetch = min(Const.maximumConcurrentFetches, allDomains.count) + let domainsToFetch = Array(allDomains.prefix(upTo: numberOfDomainsToFetch)) + allDomains = Array(allDomains.dropFirst(numberOfDomainsToFetch)) + + let handledIds = try await withThrowingTaskGroup(of: Set.self, returning: Set.self) { group in + for domain in domainsToFetch { + let url = URL(string: "\(URL.NavigationalScheme.https.separated())\(domain)") + if let idsForDomain = bookmarkDomains.ids(for: domain), let url { + group.addTask { [weak self] in + guard let self else { + return [] + } + return try await self.handleDomain(with: url, bookmarkIDs: idsForDomain) + } + } + } + + var results = Set() + for try await value in group { + results.formUnion(value) + } + return results + } + + idsToProcess.subtract(handledIds) + try stateStore.storeBookmarkIDs(idsToProcess) + + try checkCancellation() + } + } + + /** + * This function fetches a favicon for a domain specified by `url`, required for bookmarks with `bookmarkIDs`. + * + * Returns an array of procesed bookmarks, which is either the original `bookmarkIDs` in case + * of success, favicon not found or request error, or an empty array in case of cancellation + * or connection error. + */ + private func handleDomain(with url: URL, bookmarkIDs: Set) async throws -> Set { + do { + try await self.fetchAndStoreFavicon(for: url) + return bookmarkIDs + } catch is CancellationError { + return [] + } catch let error as FaviconFetchError { + switch error { + case .connectionError: + return [] + case .requestError: + return bookmarkIDs + } + } + + } + + private func fetchAndStoreFavicon(for url: URL) async throws { + let fetchResult: (Data?, URL?) + do { + fetchResult = try await fetcher.fetchFavicon(for: url) + } catch { + let nsError = error as NSError + // if user is offline, we want to retry later + let temporaryErrorCodes = [NSURLErrorNotConnectedToInternet, NSURLErrorTimedOut, NSURLErrorCancelled] + if nsError.domain == NSURLErrorDomain, temporaryErrorCodes.contains(nsError.code) { + throw FaviconFetchError.connectionError + } + throw FaviconFetchError.requestError + } + + do { + let (imageData, imageURL) = fetchResult + if let imageData { + os_log(.debug, log: log, "Favicon found for %{public}s", url.absoluteString) + try await faviconStore.storeFavicon(imageData, with: imageURL, for: url) + } else { + os_log(.debug, log: log, "Favicon not found for %{public}s", url.absoluteString) + } + + try checkCancellation() + } catch is CancellationError { + os_log(.debug, log: log, "Favicon fetching cancelled") + throw CancellationError() + } catch { + os_log(.debug, log: log, "Error storing favicon for %{public}s: %{public}s", url.absoluteString, error.localizedDescription) + throw error + } + } + + private func checkCancellation() throws { + if isCancelled { + throw CancellationError() + } + } + + private func mapBookmarkDomainsToUUIDs(for uuids: any Sequence & CVarArg) -> BookmarkDomains { + let request = BookmarkEntity.fetchRequest() + request.predicate = NSPredicate( + format: "%K IN %@ AND %K == NO AND %K == NO", + #keyPath(BookmarkEntity.uuid), uuids, + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion) + ) + request.propertiesToFetch = [#keyPath(BookmarkEntity.uuid), #keyPath(BookmarkEntity.url)] + request.relationshipKeyPathsForPrefetching = [#keyPath(BookmarkEntity.favoriteFolders), #keyPath(BookmarkEntity.parent)] + + let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) + var bookmarkDomains: BookmarkDomains! + context.performAndWait { + let bookmarks = (try? context.fetch(request)) ?? [] + bookmarkDomains = .init(bookmarks: bookmarks) + } + return bookmarkDomains + } + + private var log: OSLog { + getLog() + } + private let getLog: () -> OSLog + + // MARK: - Overrides + + override var isAsynchronous: Bool { true } + + override var isExecuting: Bool { + get { + lock.lock() + defer { lock.unlock() } + return _isExecuting + } + set { + lock.lock() + defer { lock.unlock() } + willChangeValue(forKey: #keyPath(isExecuting)) + _isExecuting = newValue + didChangeValue(forKey: #keyPath(isExecuting)) + } + } + + override var isFinished: Bool { + get { + lock.lock() + defer { lock.unlock() } + return _isFinished + } + set { + lock.lock() + defer { lock.unlock() } + willChangeValue(forKey: #keyPath(isFinished)) + _isFinished = newValue + didChangeValue(forKey: #keyPath(isFinished)) + } + } + + private let lock = NSRecursiveLock() + + private let database: CoreDataDatabase + private let stateStore: BookmarksFaviconsFetcherStateStoring + private let fetcher: FaviconFetching + private let faviconStore: FaviconStoring + + private var _isExecuting: Bool = false + private var _isFinished: Bool = false + private var _didStart: (() -> Void)? + private var _didFinish: ((Error?) -> Void)? + private var _didReceiveHTTPRequestError: ((Error) -> Void)? +} + +/** + * Helper struct that helps organize bookmarks to be processed by the fetch operation. + * + * Fetcher first processes favorites, then top level bookmarks and then all other bookmarks. + */ +struct BookmarkDomains { + var favoritesDomainsToUUIDs: [String: Set] + var topLevelBookmarksDomainsToUUIDs: [String: Set] + var otherBookmarksDomainsToUUIDs: [String: Set] + + func ids(for domain: String) -> Set? { + favoritesDomainsToUUIDs[domain] ?? topLevelBookmarksDomainsToUUIDs[domain] ?? otherBookmarksDomainsToUUIDs[domain] + } + + var allDomains: [String] { + Array(favoritesDomainsToUUIDs.keys) + topLevelBookmarksDomainsToUUIDs.keys + otherBookmarksDomainsToUUIDs.keys + } + + var allUUIDs: Set { + Set(otherBookmarksDomainsToUUIDs.values.flatMap { $0 }) + .union(favoritesDomainsToUUIDs.values.flatMap { $0 }) + .union(topLevelBookmarksDomainsToUUIDs.values.flatMap { $0 }) + } + + mutating func filterDomains(by isIncluded: (String) -> Bool) { + otherBookmarksDomainsToUUIDs = otherBookmarksDomainsToUUIDs.filter { isIncluded($0.key) } + favoritesDomainsToUUIDs = favoritesDomainsToUUIDs.filter { isIncluded($0.key) } + topLevelBookmarksDomainsToUUIDs = topLevelBookmarksDomainsToUUIDs.filter { isIncluded($0.key) } + } + + init(bookmarks: [BookmarkEntity]) { + var favoritesDomainsToUUIDs = [String: Set]() + var topLevelBookmarksDomainsToUUIDs = [String: Set]() + var otherBookmarksDomainsToUUIDs = [String: Set]() + + bookmarks.forEach { bookmark in + guard let uuid = bookmark.uuid, let domain = bookmark.url.flatMap(URL.init(string:))?.host else { + return + } + + if let favoritesUUIDs = favoritesDomainsToUUIDs[domain] { + favoritesDomainsToUUIDs[domain] = favoritesUUIDs.union([uuid]) + } else if (bookmark.favoriteFolders?.count ?? 0) > 0 { + let topLevelUUIDs = topLevelBookmarksDomainsToUUIDs.removeValue(forKey: domain) ?? [] + let otherUUIDs = otherBookmarksDomainsToUUIDs.removeValue(forKey: domain) ?? [] + favoritesDomainsToUUIDs[domain] = topLevelUUIDs.union(otherUUIDs).union([uuid]) + } else if let topLevelUUIDs = topLevelBookmarksDomainsToUUIDs[domain] { + topLevelBookmarksDomainsToUUIDs[domain] = topLevelUUIDs.union([uuid]) + } else if bookmark.parent?.uuid == BookmarkEntity.Constants.rootFolderID { + let otherUUIDs = otherBookmarksDomainsToUUIDs.removeValue(forKey: domain) ?? [] + topLevelBookmarksDomainsToUUIDs[domain] = otherUUIDs.union([uuid]) + } else if let uuids = otherBookmarksDomainsToUUIDs[domain] { + otherBookmarksDomainsToUUIDs[domain] = uuids.union([uuid]) + } else { + otherBookmarksDomainsToUUIDs[domain] = [uuid] + } + } + + self.init( + favoritesDomainsToUUIDs: favoritesDomainsToUUIDs, + topLevelBookmarksDomainsToUUIDs: topLevelBookmarksDomainsToUUIDs, + otherBookmarksDomainsToUUIDs: otherBookmarksDomainsToUUIDs + ) + } + + init( + favoritesDomainsToUUIDs: [String: Set], + topLevelBookmarksDomainsToUUIDs: [String: Set], + otherBookmarksDomainsToUUIDs: [String: Set]) { + self.favoritesDomainsToUUIDs = favoritesDomainsToUUIDs + self.topLevelBookmarksDomainsToUUIDs = topLevelBookmarksDomainsToUUIDs + self.otherBookmarksDomainsToUUIDs = otherBookmarksDomainsToUUIDs + } +} diff --git a/Sources/DDGSync/DataProvider.swift b/Sources/DDGSync/DataProvider.swift index 50c391b5a..3fca66c76 100644 --- a/Sources/DDGSync/DataProvider.swift +++ b/Sources/DDGSync/DataProvider.swift @@ -177,8 +177,38 @@ public protocol DataProviding: AnyObject { */ open class DataProvider: DataProviding { + public enum SyncResult: Equatable { + case noData + case someNewData + case newData(modifiedIds: Set, deletedIds: Set) + + public var hasNewData: Bool { + switch self { + case .noData: + return false + case .someNewData, .newData: + return true + } + } + + public var modifiedIds: Set { + guard case .newData(let modifiedIds, _) = self else { + return [] + } + return modifiedIds + } + + public var deletedIds: Set { + guard case .newData(_, let deletedIds) = self else { + return [] + } + return deletedIds + } + } + public let feature: Feature - public let syncDidUpdateData: () -> Void + public var syncDidUpdateData: () -> Void + public var syncDidFinish: () -> Void public let syncErrorPublisher: AnyPublisher public var isFeatureRegistered: Bool { @@ -210,10 +240,16 @@ open class DataProvider: DataProviding { } } - public init(feature: Feature, metadataStore: SyncMetadataStore, syncDidUpdateData: @escaping () -> Void) { + public init( + feature: Feature, + metadataStore: SyncMetadataStore, + syncDidUpdateData: @escaping () -> Void = {}, + syncDidFinish: @escaping () -> Void = {} + ) { self.feature = feature self.metadataStore = metadataStore self.syncDidUpdateData = syncDidUpdateData + self.syncDidFinish = syncDidFinish self.syncErrorPublisher = syncErrorSubject.eraseToAnyPublisher() } diff --git a/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift b/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift index d4facfa1e..b9ea91f87 100644 --- a/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift +++ b/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift @@ -23,12 +23,27 @@ import CoreData import DDGSync import Persistence +public struct FaviconsFetcherInput { + public var modifiedBookmarksUUIDs: Set + public var deletedBookmarksUUIDs: Set +} + // swiftlint:disable line_length public final class BookmarksProvider: DataProvider { - public init(database: CoreDataDatabase, metadataStore: SyncMetadataStore, syncDidUpdateData: @escaping () -> Void) { + public private(set) var faviconsFetcherInput: FaviconsFetcherInput = .init(modifiedBookmarksUUIDs: [], deletedBookmarksUUIDs: []) + + public init( + database: CoreDataDatabase, + metadataStore: SyncMetadataStore, + syncDidUpdateData: @escaping () -> Void, + syncDidFinish: @escaping (FaviconsFetcherInput?) -> Void + ) { self.database = database super.init(feature: .init(name: "bookmarks"), metadataStore: metadataStore, syncDidUpdateData: syncDidUpdateData) + self.syncDidFinish = { [weak self] in + syncDidFinish(self?.faviconsFetcherInput) + } } // MARK: - DataProviding @@ -99,6 +114,8 @@ public final class BookmarksProvider: DataProvider { ) let idsOfItemsToClearModifiedAt = cleanUpSentItems(sent, receivedUUIDs: Set(responseHandler.receivedByUUID.keys), clientTimestamp: clientTimestamp, in: context) try responseHandler.processReceivedBookmarks() + faviconsFetcherInput.modifiedBookmarksUUIDs = responseHandler.idsOfBookmarksWithModifiedURLs + faviconsFetcherInput.deletedBookmarksUUIDs = responseHandler.idsOfDeletedBookmarks #if DEBUG willSaveContextAfterApplyingSyncResponse() @@ -130,6 +147,7 @@ public final class BookmarksProvider: DataProvider { lastSyncTimestamp = serverTimestamp syncDidUpdateData() } + syncDidFinish() } func cleanUpSentItems(_ sent: [Syncable], receivedUUIDs: Set, clientTimestamp: Date, in context: NSManagedObjectContext) -> Set { @@ -182,8 +200,9 @@ public final class BookmarksProvider: DataProvider { private func clearModifiedAtAndSaveContext(uuids: Set, clientTimestamp: Date, in context: NSManagedObjectContext) throws { let insertedObjects = Array(context.insertedObjects).compactMap { $0 as? BookmarkEntity } let updatedObjects = Array(context.updatedObjects.subtracting(context.deletedObjects)).compactMap { $0 as? BookmarkEntity } + let modifiedObjects = insertedObjects + updatedObjects - (insertedObjects + updatedObjects).forEach { bookmarkEntity in + modifiedObjects.forEach { bookmarkEntity in if let uuid = bookmarkEntity.uuid, uuids.contains(uuid) { bookmarkEntity.shouldManageModifiedAt = false if let modifiedAt = bookmarkEntity.modifiedAt, modifiedAt < clientTimestamp { @@ -192,7 +211,7 @@ public final class BookmarksProvider: DataProvider { } } try context.save() - } + } private let database: CoreDataDatabase diff --git a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift index ce4353f3c..584402b0f 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift @@ -39,6 +39,9 @@ final class BookmarksResponseHandler { var idsOfItemsThatRetainModifiedAt = Set() var deduplicatedFolderUUIDs = Set() + var idsOfBookmarksWithModifiedURLs = Set() + var idsOfDeletedBookmarks = Set() + private let decrypt: (String) throws -> String init(received: [Syncable], clientTimestamp: Date? = nil, context: NSManagedObjectContext, crypter: Crypting, deduplicateEntities: Bool) throws { @@ -121,15 +124,6 @@ final class BookmarksResponseHandler { return } -// guard let favoritesUUIDs else { -// return -// } -// -// guard let favoritesFolder = BookmarkUtils.fetchFavoritesFolder(context) else { -// // Error - unable to process favorites -// return -// } - // For non-first sync we rely fully on the server response if !shouldDeduplicateEntities { favoritesFolder.favoritesArray.forEach { $0.removeFromFavorites(favoritesRoot: favoritesFolder) } @@ -235,7 +229,7 @@ final class BookmarksResponseHandler { return modifiedAt > clientTimestamp }() if !isModifiedAfterSyncTimestamp { - try existingEntity.update(with: syncable, in: context, decryptedUsing: decrypt) + try updateEntity(existingEntity, with: syncable) } if parent != nil, !existingEntity.isDeleted { @@ -249,9 +243,20 @@ final class BookmarksResponseHandler { let newEntity = BookmarkEntity.make(withUUID: syncableUUID, isFolder: syncable.isFolder, in: context) parent?.addToChildren(newEntity) - try newEntity.update(with: syncable, in: context, decryptedUsing: decrypt) + try updateEntity(newEntity, with: syncable) entitiesByUUID[syncableUUID] = newEntity } } + private func updateEntity(_ entity: BookmarkEntity, with syncable: SyncableBookmarkAdapter) throws { + let url = entity.url + try entity.update(with: syncable, in: context, decryptedUsing: decrypt) + if let uuid = entity.uuid { + if entity.isDeleted { + idsOfDeletedBookmarks.insert(uuid) + } else if entity.url != url { + idsOfBookmarksWithModifiedURLs.insert(uuid) + } + } + } } diff --git a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift index 15b1baf83..2b8019594 100644 --- a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift +++ b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift @@ -182,6 +182,7 @@ public final class CredentialsProvider: DataProvider { lastSyncTimestamp = serverTimestamp syncDidUpdateData() } + syncDidFinish() } func cleanUpSentItems(_ sent: [Syncable], receivedUUIDs: Set, clientTimestamp: Date, in database: Database) throws -> Set { diff --git a/Sources/SyncDataProviders/Settings/SettingsProvider.swift b/Sources/SyncDataProviders/Settings/SettingsProvider.swift index fc59bb1fd..f097365ec 100644 --- a/Sources/SyncDataProviders/Settings/SettingsProvider.swift +++ b/Sources/SyncDataProviders/Settings/SettingsProvider.swift @@ -261,6 +261,7 @@ public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { lastSyncTimestamp = serverTimestamp syncDidUpdateData() } + syncDidFinish() } func cleanUpSentItems( diff --git a/Tests/BookmarksTests/FaviconsFetcher/BookmarkDomainsTests.swift b/Tests/BookmarksTests/FaviconsFetcher/BookmarkDomainsTests.swift new file mode 100644 index 000000000..02250ca6e --- /dev/null +++ b/Tests/BookmarksTests/FaviconsFetcher/BookmarkDomainsTests.swift @@ -0,0 +1,181 @@ +// +// BookmarkDomainsTests.swift +// DuckDuckGo +// +// 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 BookmarksTestsUtils +import Foundation +import Persistence +import XCTest +@testable import Bookmarks + +final class BookmarkDomainsTests: XCTestCase { + var bookmarksDatabase: CoreDataDatabase! + var location: URL! + + override func setUp() { + super.setUp() + + location = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + let bundle = Bookmarks.bundle + guard let model = CoreDataDatabase.loadModel(from: bundle, named: "BookmarksModel") else { + XCTFail("Failed to load model") + return + } + bookmarksDatabase = CoreDataDatabase(name: className, containerLocation: location, model: model) + bookmarksDatabase.loadStore() + } + + override func tearDown() { + super.tearDown() + + try? bookmarksDatabase.tearDown(deleteStores: true) + bookmarksDatabase = nil + try? FileManager.default.removeItem(at: location) + } + + func testThatAllFavoritesDictionariesKeysAreDisjoint() throws { + let bookmarkDomains = populateDatabaseAndMakeBookmarkDomains { + Bookmark(id: "1", url: "https://1.com") + Bookmark(id: "2", url: "https://2.com") + Folder { + Bookmark(id: "3", url: "https://3.com") + Bookmark(id: "4", url: "https://4.com") + Bookmark(id: "5", url: "https://5.com", favoritedOn: [.mobile, .unified]) + Bookmark(id: "6", url: "https://6.com") + } + Bookmark(id: "7", url: "https://7.com") + } + + XCTAssertEqual(bookmarkDomains.favoritesDomainsToUUIDs, ["5.com": ["5"]]) + XCTAssertEqual(bookmarkDomains.topLevelBookmarksDomainsToUUIDs, [ + "1.com": ["1"], + "2.com": ["2"], + "7.com": ["7"] + ]) + XCTAssertEqual(bookmarkDomains.otherBookmarksDomainsToUUIDs, [ + "3.com": ["3"], + "4.com": ["4"], + "6.com": ["6"] + ]) + XCTAssertEqual(Set(bookmarkDomains.allDomains), ["1.com", "2.com", "3.com", "4.com", "5.com", "6.com", "7.com"]) + XCTAssertEqual(Set(bookmarkDomains.allUUIDs), ["1", "2", "3", "4", "5", "6", "7"]) + } + + func testThatFavoritesDomainMayContainTopLevelOrOtherBookmarksUUIDs() throws { + let bookmarkDomains = populateDatabaseAndMakeBookmarkDomains { + Bookmark(id: "1", url: "https://1.com/1") + Bookmark(id: "2", url: "https://1.com/2") + Folder { + Bookmark(id: "3", url: "https://1.com/3") + Bookmark(id: "4", url: "https://1.com/4") + Bookmark(id: "5", url: "https://1.com/5", favoritedOn: [.mobile, .unified]) + Bookmark(id: "6", url: "https://2.com/6") + } + Bookmark(id: "7", url: "https://2.com/7") + } + XCTAssertEqual(bookmarkDomains.favoritesDomainsToUUIDs, ["1.com": ["1", "2", "3", "4", "5"]]) + XCTAssertEqual(bookmarkDomains.topLevelBookmarksDomainsToUUIDs, ["2.com": ["6", "7"]]) + XCTAssertEqual(bookmarkDomains.otherBookmarksDomainsToUUIDs, [:]) + XCTAssertEqual(Set(bookmarkDomains.allDomains), ["1.com", "2.com"]) + XCTAssertEqual(Set(bookmarkDomains.allUUIDs), ["1", "2", "3", "4", "5", "6", "7"]) + } + + func testThatTopLevelDomainMayContainOtherBookmarksUUIDs() throws { + let bookmarkDomains = populateDatabaseAndMakeBookmarkDomains { + Bookmark(id: "1", url: "https://1.com/1") + Bookmark(id: "2", url: "https://1.com/2") + Folder { + Bookmark(id: "3", url: "https://1.com/3") + Bookmark(id: "4", url: "https://1.com/4") + Bookmark(id: "5", url: "https://1.com/5") + Bookmark(id: "6", url: "https://2.com/6") + } + Bookmark(id: "7", url: "https://2.com/7") + } + XCTAssertEqual(bookmarkDomains.favoritesDomainsToUUIDs, [:]) + XCTAssertEqual(bookmarkDomains.topLevelBookmarksDomainsToUUIDs, [ + "1.com": ["1", "2", "3", "4", "5"], + "2.com": ["6", "7"] + ]) + XCTAssertEqual(bookmarkDomains.otherBookmarksDomainsToUUIDs, [:]) + XCTAssertEqual(Set(bookmarkDomains.allDomains), ["1.com", "2.com"]) + XCTAssertEqual(Set(bookmarkDomains.allUUIDs), ["1", "2", "3", "4", "5", "6", "7"]) + } + + func testThatAllDomainsHasUniqueEntries() throws { + let bookmarkDomains = populateDatabaseAndMakeBookmarkDomains { + Bookmark(id: "1", url: "https://1.com/1") + Bookmark(id: "2", url: "https://2.com/1") + Bookmark(id: "3", url: "https://3.com/1") + Folder { + Bookmark(id: "4", url: "https://1.com/4") + Bookmark(id: "5", url: "https://2.com/5") + Bookmark(id: "6", url: "https://3.com/6") + Bookmark(id: "7", url: "https://1.com/4", favoritedOn: [.mobile, .unified]) + Bookmark(id: "8", url: "https://2.com/5", favoritedOn: [.mobile, .unified]) + Bookmark(id: "9", url: "https://3.com/6", favoritedOn: [.mobile, .unified]) + } + } + XCTAssertEqual(bookmarkDomains.favoritesDomainsToUUIDs, [ + "1.com": ["1", "4", "7"], + "2.com": ["2", "5", "8"], + "3.com": ["3", "6", "9"] + ]) + XCTAssertEqual(bookmarkDomains.topLevelBookmarksDomainsToUUIDs, [:]) + XCTAssertEqual(bookmarkDomains.otherBookmarksDomainsToUUIDs, [:]) + XCTAssertEqual(Set(bookmarkDomains.allDomains), ["1.com", "2.com", "3.com"]) + XCTAssertEqual(Set(bookmarkDomains.allUUIDs), ["1", "2", "3", "4", "5", "6", "7", "8", "9"]) + } + + // MARK: - Private + + private func populateDatabaseAndMakeBookmarkDomains(@BookmarkTreeBuilder with builder: () -> [BookmarkTreeNode]) -> BookmarkDomains { + let bookmarkTree = BookmarkTree(builder: builder) + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + var bookmarkDomains: BookmarkDomains! + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() + bookmarkDomains = BookmarkDomains.make(withAllBookmarksIn: context) + } + return bookmarkDomains + } +} + +private extension BookmarkDomains { + + static func make(withAllBookmarksIn context: NSManagedObjectContext) -> BookmarkDomains { + let request = BookmarkEntity.fetchRequest() + request.predicate = NSPredicate( + format: "%K == NO AND %K == NO", + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion) + ) + request.propertiesToFetch = [#keyPath(BookmarkEntity.url)] + request.relationshipKeyPathsForPrefetching = [#keyPath(BookmarkEntity.favoriteFolders), #keyPath(BookmarkEntity.parent)] + + var bookmarkDomains: BookmarkDomains! + context.performAndWait { + let bookmarks = (try? context.fetch(request)) ?? [] + bookmarkDomains = .init(bookmarks: bookmarks) + } + return bookmarkDomains + } +} diff --git a/Tests/BookmarksTests/FaviconsFetcher/BookmarksFaviconsFetcherTests.swift b/Tests/BookmarksTests/FaviconsFetcher/BookmarksFaviconsFetcherTests.swift new file mode 100644 index 000000000..47c89e74d --- /dev/null +++ b/Tests/BookmarksTests/FaviconsFetcher/BookmarksFaviconsFetcherTests.swift @@ -0,0 +1,299 @@ +// +// BookmarksFaviconsFetcherTests.swift +// DuckDuckGo +// +// 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 BookmarksTestsUtils +import Common +import Foundation +import Persistence +import XCTest +@testable import Bookmarks + +final class MockBookmarksFaviconsFetcherEventMapper: EventMapping { + static var errors: [BookmarksFaviconsFetcherError] = [] + + public init() { + super.init { event, _, _, _ in + Self.errors.append(event) + } + } + + override init(mapping: @escaping EventMapping.Mapping) { + fatalError("Use init()") + } +} + +final class BookmarksFaviconsFetcherTests: XCTestCase { + var bookmarksDatabase: CoreDataDatabase! + var location: URL! + + var stateStore: MockFetcherStateStore! + var fetcher: MockFaviconFetcher! + var faviconStore: MockFaviconStore! + let eventMapper = MockBookmarksFaviconsFetcherEventMapper() + + var bookmarksFaviconsFetcher: BookmarksFaviconsFetcher! + + override func setUp() { + super.setUp() + + location = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + let bundle = Bookmarks.bundle + guard let model = CoreDataDatabase.loadModel(from: bundle, named: "BookmarksModel") else { + XCTFail("Failed to load model") + return + } + bookmarksDatabase = CoreDataDatabase(name: className, containerLocation: location, model: model) + bookmarksDatabase.loadStore() + + faviconStore = MockFaviconStore() + fetcher = MockFaviconFetcher() + stateStore = MockFetcherStateStore() + MockBookmarksFaviconsFetcherEventMapper.errors = [] + + bookmarksFaviconsFetcher = BookmarksFaviconsFetcher( + database: bookmarksDatabase, + stateStore: stateStore, + fetcher: fetcher, + faviconStore: faviconStore, + errorEvents: eventMapper, + log: .default + ) + } + + override func tearDown() { + super.tearDown() + + try? bookmarksDatabase.tearDown(deleteStores: true) + bookmarksDatabase = nil + try? FileManager.default.removeItem(at: location) + } + + func testThatUpdateBookmarkIDsMakesUnionWithModifiedIDsAndSubtractsDeletedIDs() async throws { + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + bookmarksFaviconsFetcher.updateBookmarkIDs(modified: ["4", "5", "6"], deleted: []) + await runAfterOperationsFinished { + let ids = (try? self.stateStore.getBookmarkIDs()) ?? [] + XCTAssertEqual(ids, ["1", "2", "3", "4", "5", "6"]) + XCTAssertTrue(MockBookmarksFaviconsFetcherEventMapper.errors.isEmpty) + } + + bookmarksFaviconsFetcher.updateBookmarkIDs(modified: ["2", "3", "4"], deleted: []) + await runAfterOperationsFinished { + let ids = (try? self.stateStore.getBookmarkIDs()) ?? [] + XCTAssertEqual(ids, ["1", "2", "3", "4", "5", "6"]) + XCTAssertTrue(MockBookmarksFaviconsFetcherEventMapper.errors.isEmpty) + } + + bookmarksFaviconsFetcher.updateBookmarkIDs(modified: ["5", "6", "7"], deleted: ["1", "2", "3", "4"]) + await runAfterOperationsFinished { + let ids = (try? self.stateStore.getBookmarkIDs()) ?? [] + XCTAssertEqual(ids, ["5", "6", "7"]) + XCTAssertTrue(MockBookmarksFaviconsFetcherEventMapper.errors.isEmpty) + } + + bookmarksFaviconsFetcher.updateBookmarkIDs(modified: [], deleted: ["8"]) + await runAfterOperationsFinished { + let ids = (try? self.stateStore.getBookmarkIDs()) ?? [] + XCTAssertEqual(ids, ["5", "6", "7"]) + XCTAssertTrue(MockBookmarksFaviconsFetcherEventMapper.errors.isEmpty) + } + } + + func testThatStateStoreSaveErrorIsReportedToEventMapperOnUpdateBookmarkIDs() async throws { + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + stateStore.storeError = BookmarksFaviconsFetcherError.failedToStoreBookmarkIDs(StoreError()) + + bookmarksFaviconsFetcher.updateBookmarkIDs(modified: ["4", "5", "6"], deleted: []) + await runAfterOperationsFinished { + let ids = (try? self.stateStore.getBookmarkIDs()) ?? [] + XCTAssertEqual(ids, ["1", "2", "3"]) + XCTAssertEqual(MockBookmarksFaviconsFetcherEventMapper.errors.count, 1) + let error = MockBookmarksFaviconsFetcherEventMapper.errors.first + guard case .failedToStoreBookmarkIDs = error else { + XCTFail("Unexpected error") + return + } + } + } + + func testThatStateStoreRetrieveErrorIsReportedToEventMapperOnUpdateBookmarkIDs() async throws { + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + stateStore.getError = BookmarksFaviconsFetcherError.failedToRetrieveBookmarkIDs(StoreError()) + + bookmarksFaviconsFetcher.updateBookmarkIDs(modified: ["4", "5", "6"], deleted: []) + await runAfterOperationsFinished { + self.stateStore.getError = nil + let ids = (try? self.stateStore.getBookmarkIDs()) ?? [] + XCTAssertEqual(ids, ["1", "2", "3"]) + XCTAssertEqual(MockBookmarksFaviconsFetcherEventMapper.errors.count, 1) + let error = MockBookmarksFaviconsFetcherEventMapper.errors.first + guard case .failedToRetrieveBookmarkIDs = error else { + XCTFail("Unexpected error") + return + } + } + } + + func testWhenThereAreNoBookmarksThenInitializeFetcherStateStoresEmptySet() async throws { + populateBookmarks {} + + bookmarksFaviconsFetcher.initializeFetcherState() + await runAfterOperationsFinished { + let ids = (try? self.stateStore.getBookmarkIDs()) ?? [] + XCTAssertTrue(ids.isEmpty) + XCTAssertTrue(MockBookmarksFaviconsFetcherEventMapper.errors.isEmpty) + } + } + + func testThatInitializeFetcherStateStoresAllBookmarkIDs() async throws { + populateBookmarks { + Folder(id: "1") {} + Folder(id: "2") {} + Folder(id: "3") { + Bookmark(id: "4") + Bookmark(id: "5") + Folder(id: "6") { + Bookmark(id: "7") + Bookmark(id: "8", isDeleted: true) + } + } + Bookmark(id: "9") + } + + bookmarksFaviconsFetcher.initializeFetcherState() + await runAfterOperationsFinished { + let ids = (try? self.stateStore.getBookmarkIDs()) ?? [] + XCTAssertEqual(ids, ["4", "5", "7", "9"]) + XCTAssertTrue(MockBookmarksFaviconsFetcherEventMapper.errors.isEmpty) + } + } + + func testWhenFetchingIsFinishedThenDidFinishPublisherEmitsEvent() async throws { + var results: [Result] = [] + let cancellable = bookmarksFaviconsFetcher.fetchingDidFinishPublisher.sink { results.append($0) } + + bookmarksFaviconsFetcher.startFetching() + + await runAfterOperationsFinished { + XCTAssertEqual(results.count, 1) + guard case .success = results.first else { + XCTFail("Expected success") + return + } + } + cancellable.cancel() + } + + func testThatOperationCanBeCancelled() async throws { + populateBookmarks { + Bookmark(id: "1", url: "https://duckduckgo.com") + } + stateStore.bookmarkIDs = ["1"] + fetcher.fetchFavicon = { _ in + try await Task.sleep(nanoseconds: 100_000_000) + return (nil, nil) + } + + var results: [Result] = [] + let cancellable = bookmarksFaviconsFetcher.fetchingDidFinishPublisher.sink { results.append($0) } + + bookmarksFaviconsFetcher.startFetching() + try await Task.sleep(nanoseconds: 10_000_000) + bookmarksFaviconsFetcher.cancelOngoingFetchingIfNeeded() + + await runAfterOperationsFinished { + XCTAssertEqual(results.count, 1) + guard case .failure = results.first else { + XCTFail("Expected failure") + return + } + XCTAssertEqual(MockBookmarksFaviconsFetcherEventMapper.errors.count, 1) + guard MockBookmarksFaviconsFetcherEventMapper.errors[0].underlyingError is CancellationError else { + XCTFail("Expected CancellationError") + return + } + } + cancellable.cancel() + } + + func testThatCallToStartFetchingCancelsAnyRunningOperation() async throws { + populateBookmarks { + Bookmark(id: "1", url: "https://duckduckgo.com") + } + stateStore.bookmarkIDs = ["1"] + fetcher.fetchFavicon = { _ in + try await Task.sleep(nanoseconds: 100_000_000) + return (nil, nil) + } + + var results: [Result] = [] + var isInProgressEvents: [Bool] = [] + let didFinishCancellable = bookmarksFaviconsFetcher.fetchingDidFinishPublisher.sink { results.append($0) } + let isInProgressCancellable = bookmarksFaviconsFetcher.$isFetchingInProgress.sink { isInProgressEvents.append($0) } + + await withTaskGroup(of: Void.self) { group in + group.addTask { + self.bookmarksFaviconsFetcher.startFetching() + } + try? await Task.sleep(nanoseconds: 10_000_000) + group.addTask { + self.bookmarksFaviconsFetcher.startFetching() + } + } + + await runAfterOperationsFinished { + let successfulRuns = results.filter { result in + if case .success = result { + return true + } + return false + } + XCTAssertEqual(successfulRuns.count, 1) + XCTAssertEqual(results.count, 2) + XCTAssertEqual(isInProgressEvents, [false, true, false, true, false]) + } + didFinishCancellable.cancel() + isInProgressCancellable.cancel() + } + + // MARK: - Private + + private func runAfterOperationsFinished(_ block: @escaping () -> Void) async { + await withCheckedContinuation { continuation in + bookmarksFaviconsFetcher.operationQueue.addBarrierBlock { + block() + continuation.resume() + } + } + } + + private func populateBookmarks(@BookmarkTreeBuilder _ builder: () -> [BookmarkTreeNode]) { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree(builder: builder) + + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() + } + } +} diff --git a/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetchOperationTests.swift b/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetchOperationTests.swift new file mode 100644 index 000000000..2da747b28 --- /dev/null +++ b/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetchOperationTests.swift @@ -0,0 +1,311 @@ +// +// FaviconsFetchOperationTests.swift +// DuckDuckGo +// +// 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 BookmarksTestsUtils +import Common +import Foundation +import Persistence +import XCTest +@testable import Bookmarks + +final class FaviconsFetchOperationTests: XCTestCase { + var bookmarksDatabase: CoreDataDatabase! + var location: URL! + + var stateStore: MockFetcherStateStore! + var fetcher: MockFaviconFetcher! + var faviconStore: MockFaviconStore! + + var fetchOperation: FaviconsFetchOperation! + + override func setUp() { + super.setUp() + + location = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + let bundle = Bookmarks.bundle + guard let model = CoreDataDatabase.loadModel(from: bundle, named: "BookmarksModel") else { + XCTFail("Failed to load model") + return + } + bookmarksDatabase = CoreDataDatabase(name: className, containerLocation: location, model: model) + bookmarksDatabase.loadStore() + + faviconStore = MockFaviconStore() + fetcher = MockFaviconFetcher() + stateStore = MockFetcherStateStore() + + fetchOperation = FaviconsFetchOperation( + database: bookmarksDatabase, + stateStore: stateStore, + fetcher: fetcher, + faviconStore: faviconStore + ) + } + + override func tearDown() { + super.tearDown() + + try? bookmarksDatabase.tearDown(deleteStores: true) + bookmarksDatabase = nil + try? FileManager.default.removeItem(at: location) + } + + func testWhenGetBookmarkIDsThrowsErrorThenOperationThrowsError() async throws { + stateStore.getError = StoreError() + do { + try await fetchOperation.fetchFavicons() + XCTFail("Expected to throw error") + } catch {} + } + + func testThatMissingFaviconsAreFetchedAndStored() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + let fetchFaviconExpectation = expectation(description: "fetchFavicon") + let storeFaviconExpectation = expectation(description: "storeFavicon") + fetchFaviconExpectation.expectedFulfillmentCount = 3 + storeFaviconExpectation.expectedFulfillmentCount = 3 + + fetcher.fetchFavicon = { _ in + fetchFaviconExpectation.fulfill() + return (Data(), nil) + } + + faviconStore.storeFavicon = { _, _, _ in + storeFaviconExpectation.fulfill() + } + + try await fetchOperation.fetchFavicons() + + await fulfillment(of: [fetchFaviconExpectation, storeFaviconExpectation], timeout: 0.1) + XCTAssertTrue(try stateStore.getBookmarkIDs().isEmpty) + } + + func testWhenBookmarkHasFaviconThenItIsNotFetched() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + let fetchFaviconExpectation = expectation(description: "fetchFavicon") + let storeFaviconExpectation = expectation(description: "storeFavicon") + fetchFaviconExpectation.expectedFulfillmentCount = 2 + storeFaviconExpectation.expectedFulfillmentCount = 2 + + fetcher.fetchFavicon = { _ in + fetchFaviconExpectation.fulfill() + return (Data(), nil) + } + + faviconStore.hasFavicon = { domain in + return domain == "1.com" + } + + faviconStore.storeFavicon = { _, _, _ in + storeFaviconExpectation.fulfill() + } + + try await fetchOperation.fetchFavicons() + + await fulfillment(of: [fetchFaviconExpectation, storeFaviconExpectation], timeout: 0.1) + XCTAssertTrue(try stateStore.getBookmarkIDs().isEmpty) + } + + func testWhenStateIsEmptyThenFaviconsAreNotFetched() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs([]) + + let fetchFaviconExpectation = expectation(description: "fetchFavicon") + fetchFaviconExpectation.isInverted = true + + fetcher.fetchFavicon = { _ in + fetchFaviconExpectation.fulfill() + return (Data(), nil) + } + + try await fetchOperation.fetchFavicons() + + await fulfillment(of: [fetchFaviconExpectation], timeout: 0.1) + } + + func testWhenBookmarkWithIDIsNotPresentThenItIsIgnored() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["4"]) + + let fetchFaviconExpectation = expectation(description: "fetchFavicon") + fetchFaviconExpectation.isInverted = true + + fetcher.fetchFavicon = { _ in + fetchFaviconExpectation.fulfill() + return (Data(), nil) + } + + try await fetchOperation.fetchFavicons() + + await fulfillment(of: [fetchFaviconExpectation], timeout: 0.1) + XCTAssertTrue(try stateStore.getBookmarkIDs().isEmpty) + } + + func testThatOnlyOneRequestPerDomainIsMade() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark(id: "1", url: "https://duckduckgo.com") + Bookmark(id: "2", url: "https://wikipedia.org") + Bookmark(id: "3", url: "https://google.com") + Bookmark(id: "4", url: "https://duckduckgo.com/1") + Bookmark(id: "5", url: "https://wikipedia.org/2") + Bookmark(id: "6", url: "https://google.com/3") + } + + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() + } + + try stateStore.storeBookmarkIDs(["1", "2", "3", "4", "5", "6"]) + + let fetchFaviconExpectation = expectation(description: "fetchFavicon") + let storeFaviconExpectation = expectation(description: "storeFavicon") + fetchFaviconExpectation.expectedFulfillmentCount = 3 + storeFaviconExpectation.expectedFulfillmentCount = 3 + + fetcher.fetchFavicon = { _ in + fetchFaviconExpectation.fulfill() + return (Data(), nil) + } + + faviconStore.storeFavicon = { _, _, _ in + storeFaviconExpectation.fulfill() + } + + try await fetchOperation.fetchFavicons() + + await fulfillment(of: [fetchFaviconExpectation, storeFaviconExpectation], timeout: 0.1) + XCTAssertTrue(try stateStore.getBookmarkIDs().isEmpty) + } + + func testWhenFaviconIsNotFoundThenItIsRemovedFromState() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + fetcher.fetchFavicon = { _ in + return (nil, nil) + } + + try await fetchOperation.fetchFavicons() + XCTAssertTrue(try stateStore.getBookmarkIDs().isEmpty) + } + + func testWhenFaviconFetchingThrowsNoInternetErrorThenItIsNotRemovedFromState() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + fetcher.fetchFavicon = { _ in + throw NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet) + } + + try await fetchOperation.fetchFavicons() + XCTAssertEqual(try stateStore.getBookmarkIDs(), ["1", "2", "3"]) + } + + func testWhenFaviconFetchingThrowsTimeoutErrorThenItIsNotRemovedFromState() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + fetcher.fetchFavicon = { _ in + throw NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut) + } + + try await fetchOperation.fetchFavicons() + XCTAssertEqual(try stateStore.getBookmarkIDs(), ["1", "2", "3"]) + } + + func testWhenFaviconFetchingThrowsCancelledErrorThenItIsNotRemovedFromState() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + fetcher.fetchFavicon = { _ in + throw NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled) + } + + try await fetchOperation.fetchFavicons() + XCTAssertEqual(try stateStore.getBookmarkIDs(), ["1", "2", "3"]) + } + + func testWhenFaviconFetchingThrowsErrorOtherThanNoInternetThenItIsRemovedFromState() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + fetcher.fetchFavicon = { _ in + throw NSError(domain: NSURLErrorDomain, code: NSURLErrorSecureConnectionFailed) + } + + try await fetchOperation.fetchFavicons() + XCTAssertEqual(try stateStore.getBookmarkIDs(), []) + } + + func testWhenFaviconStoringThrowsErrorThenErrorIsRethrown() async throws { + populateBookmarks() + + try stateStore.storeBookmarkIDs(["1", "2", "3"]) + + fetcher.fetchFavicon = { _ in + return (Data(), nil) + } + + faviconStore.storeFavicon = { _, _, _ in + throw StoreError() + } + + do { + try await fetchOperation.fetchFavicons() + XCTFail("Expected to throw error") + } catch { + XCTAssertTrue(error is StoreError) + } + XCTAssertEqual(try stateStore.getBookmarkIDs(), ["1", "2", "3"]) + } + + private func populateBookmarks() { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark(id: "1", url: "https://1.com") + Bookmark(id: "2", url: "https://2.com") + Bookmark(id: "3", url: "https://3.com") + } + + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() + } + } +} diff --git a/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetcherMocks.swift b/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetcherMocks.swift new file mode 100644 index 000000000..7664c95a7 --- /dev/null +++ b/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetcherMocks.swift @@ -0,0 +1,64 @@ +// +// FaviconsFetcherMocks.swift +// DuckDuckGo +// +// 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 +@testable import Bookmarks + +struct StoreError: Error {} + +class MockFaviconStore: FaviconStoring { + var hasFavicon: (String) -> Bool = { _ in false } + var storeFavicon: (Data, URL?, URL) async throws -> Void = { _, _, _ in } + + func hasFavicon(for domain: String) -> Bool { + hasFavicon(domain) + } + + func storeFavicon(_ imageData: Data, with url: URL?, for documentURL: URL) async throws { + try await storeFavicon(imageData, url, documentURL) + } +} + +final class MockFaviconFetcher: FaviconFetching { + var fetchFavicon: (URL) async throws -> (Data?, URL?) = { _ in (nil, nil) } + + func fetchFavicon(for url: URL) async throws -> (Data?, URL?) { + try await fetchFavicon(url) + } +} + +final class MockFetcherStateStore: BookmarksFaviconsFetcherStateStoring { + var bookmarkIDs: Set = [] + var getError: Error? + var storeError: Error? + + func getBookmarkIDs() throws -> Set { + if let getError { + throw getError + } + return bookmarkIDs + } + + func storeBookmarkIDs(_ ids: Set) throws { + if let storeError { + throw storeError + } + bookmarkIDs = ids + } +} diff --git a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift index 9cac99338..d81d76c3f 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift @@ -727,4 +727,41 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { }) } } + + // MARK: - syncDidFinish callback + + func testThatSyncDidFinishCallbackReportsModifiedAndDeletedObjectIDs() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark("test", id: "1") + Bookmark("test2", id: "2") + } + + let received: [Syncable] = [ + .rootFolder(children: ["1", "3"]), + .bookmark("test1", id: "1"), + .bookmark("test3", id: "3"), + .bookmark(id: "2", isDeleted: true) + ] + + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() + } + + expectedSyncResult = .newData(modifiedIds: ["1", "3", "bookmarks_root"], deletedIds: ["2"]) + + try await provider.handleSyncResponse(sent: [], received: received, clientTimestamp: Date(), serverTimestamp: "1234", crypter: crypter) + + context.performAndWait { + context.refreshAllObjects() + let rootFolder = BookmarkUtils.fetchRootFolder(context)! + assertEquivalent(rootFolder, BookmarkTree { + Bookmark("test1", id: "1") + Bookmark("test3", id: "3") + }) + } + } } diff --git a/Tests/SyncDataProvidersTests/Bookmarks/helpers/BookmarksProviderTestsBase.swift b/Tests/SyncDataProvidersTests/Bookmarks/helpers/BookmarksProviderTestsBase.swift index 9a551484e..9b2c5ccf5 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/helpers/BookmarksProviderTestsBase.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/helpers/BookmarksProviderTestsBase.swift @@ -32,6 +32,8 @@ internal class BookmarksProviderTestsBase: XCTestCase { var crypter = CryptingMock() var provider: BookmarksProvider! + var expectedSyncResult: BookmarksProvider.SyncResult? + func setUpBookmarksDatabase() { bookmarksDatabaseLocation = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) @@ -62,7 +64,12 @@ internal class BookmarksProviderTestsBase: XCTestCase { setUpBookmarksDatabase() setUpSyncMetadataDatabase() - provider = BookmarksProvider(database: bookmarksDatabase, metadataStore: LocalSyncMetadataStore(database: metadataDatabase), syncDidUpdateData: {}) + provider = BookmarksProvider( + database: bookmarksDatabase, + metadataStore: LocalSyncMetadataStore(database: metadataDatabase), + syncDidUpdateData: {}, + syncDidFinish: { _ in } + ) } override func tearDown() { From fe6b0097f2f03665475473f7e235eb891ddd2f16 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Thu, 30 Nov 2023 12:11:07 +0000 Subject: [PATCH 07/39] bump privacy-dashboard to 3.0.0 (#549) Co-authored-by: Shane Osbourne --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 05a228e36..450099eed 100644 --- a/Package.resolved +++ b/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "b4ac92a444e79d5651930482623b9f6dc9265667", - "version" : "2.0.0" + "revision" : "daa9708223b4b4318fb6448ca44801dfabcddc6f", + "version" : "3.0.0" } }, { diff --git a/Package.swift b/Package.swift index cbd82bd10..89eddfd63 100644 --- a/Package.swift +++ b/Package.swift @@ -38,7 +38,7 @@ let package = Package( .package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"), .package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "2.1.0"), .package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "4.52.0"), - .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "3.0.0"), .package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"), .package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"), .package(url: "https://github.com/duckduckgo/wireguard-apple", exact: "1.1.1") From 8e77b5a057d6f941a16218b27289554b60d44772 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:03:19 -0500 Subject: [PATCH 08/39] Use VPNSettings for cohort storage (#581) --- .../PacketTunnelProvider.swift | 3 +- .../UserDefaults+vpnFirstEnabled.swift | 41 +++++++++++++++++++ .../Settings/VPNSettings.swift | 32 +++++++++++++-- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 Sources/NetworkProtection/Settings/Extensions/UserDefaults+vpnFirstEnabled.swift diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index d40b104c2..2e980e1f9 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -830,7 +830,8 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { .setNotifyStatusChanges, .setRegistrationKeyValidity, .setSelectedEnvironment, - .setShowInMenuBar: + .setShowInMenuBar, + .setVPNFirstEnabled: // Intentional no-op, as some setting changes don't require any further operation break } diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+vpnFirstEnabled.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+vpnFirstEnabled.swift new file mode 100644 index 000000000..dbf9b1f30 --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+vpnFirstEnabled.swift @@ -0,0 +1,41 @@ +// +// UserDefaults+showInMenuBar.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 Combine +import Foundation + +extension UserDefaults { + private var vpnFirstEnabledKey: String { + "vpnFirstEnabled" + } + + @objc + dynamic var vpnFirstEnabled: Date? { + get { + value(forKey: vpnFirstEnabledKey) as? Date + } + + set { + set(newValue, forKey: vpnFirstEnabledKey) + } + } + + var vpnFirstEnabledPublisher: AnyPublisher { + publisher(for: \.vpnFirstEnabled).eraseToAnyPublisher() + } +} diff --git a/Sources/NetworkProtection/Settings/VPNSettings.swift b/Sources/NetworkProtection/Settings/VPNSettings.swift index 8eeed00b4..a8b21ea94 100644 --- a/Sources/NetworkProtection/Settings/VPNSettings.swift +++ b/Sources/NetworkProtection/Settings/VPNSettings.swift @@ -19,7 +19,7 @@ import Combine import Foundation -// swiftlint:disable type_body_length +// swiftlint:disable type_body_length file_length /// Persists and publishes changes to tunnel settings. /// @@ -40,6 +40,7 @@ public final class VPNSettings { case setSelectedLocation(_ selectedLocation: SelectedLocation) case setSelectedEnvironment(_ selectedEnvironment: SelectedEnvironment) case setShowInMenuBar(_ showInMenuBar: Bool) + case setVPNFirstEnabled(_ vpnFirstEnabled: Date?) } public enum RegistrationKeyValidity: Codable { @@ -131,6 +132,10 @@ public final class VPNSettings { Change.setShowInMenuBar(showInMenuBar) }.eraseToAnyPublisher() + let vpnFirstEnabledPublisher = vpnFirstEnabledPublisher.map { vpnFirstEnabled in + Change.setVPNFirstEnabled(vpnFirstEnabled) + }.eraseToAnyPublisher() + return Publishers.MergeMany( connectOnLoginPublisher, includeAllNetworksPublisher, @@ -140,7 +145,8 @@ public final class VPNSettings { serverChangePublisher, locationChangePublisher, environmentChangePublisher, - showInMenuBarPublisher).eraseToAnyPublisher() + showInMenuBarPublisher, + vpnFirstEnabledPublisher).eraseToAnyPublisher() }() public init(defaults: UserDefaults) { @@ -163,6 +169,7 @@ public final class VPNSettings { // MARK: - Applying Changes + // swiftlint:disable cyclomatic_complexity public func apply(change: Change) { switch change { case .setConnectOnLogin(let connectOnLogin): @@ -185,8 +192,11 @@ public final class VPNSettings { self.selectedEnvironment = selectedEnvironment case .setShowInMenuBar(let showInMenuBar): self.showInMenuBar = showInMenuBar + case .setVPNFirstEnabled(let vpnFirstEnabled): + self.vpnFirstEnabled = vpnFirstEnabled } } + // swiftlint:enable cyclomatic_complexity // MARK: - Connect on Login @@ -375,6 +385,22 @@ public final class VPNSettings { } } } + + // MARK: - First time VPN is enabled + + public var vpnFirstEnabledPublisher: AnyPublisher { + defaults.vpnFirstEnabledPublisher + } + + public var vpnFirstEnabled: Date? { + get { + defaults.vpnFirstEnabled + } + + set { + defaults.vpnFirstEnabled = newValue + } + } } -// swiftlint:enable type_body_length +// swiftlint:enable type_body_length file_length From 9228f64c71e1f2ed76792482f1cfaca6ff9eb5e2 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Fri, 1 Dec 2023 23:21:27 +0100 Subject: [PATCH 09/39] Don't include port in server address (#583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/0/1206056874665890/f iOS PR: duckduckgo/iOS#2214 macOS PR: duckduckgo/macos-browser#1916 What kind of version bump will this require?: Minor Description: During the NetP iOS ship review, there was feedback that the server IP address labels should not feature the port. So now, rather than sending the host + port as the server address, I’m just sending the host. --- .../PacketTunnelProvider.swift | 2 +- .../WireGuardKit/Endpoint.swift | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 2e980e1f9..79ccc6f03 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -924,7 +924,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } private func handleGetServerAddress(completionHandler: ((Data?) -> Void)? = nil) { - let response = lastSelectedServerInfo?.endpoint.map { ExtensionMessageString($0.description) } + let response = lastSelectedServerInfo?.endpoint.map { ExtensionMessageString($0.host.description) } completionHandler?(response?.rawValue) } diff --git a/Sources/NetworkProtection/WireGuardKit/Endpoint.swift b/Sources/NetworkProtection/WireGuardKit/Endpoint.swift index 0f6339c7a..9d9502dd3 100644 --- a/Sources/NetworkProtection/WireGuardKit/Endpoint.swift +++ b/Sources/NetworkProtection/WireGuardKit/Endpoint.swift @@ -30,16 +30,7 @@ extension Endpoint: Hashable { extension Endpoint: CustomStringConvertible { public var description: String { - switch host { - case .name(let hostname, _): - return "\(hostname):\(port)" - case .ipv4(let address): - return "\(address):\(port)" - case .ipv6(let address): - return "[\(address)]:\(port)" - @unknown default: - fatalError() - } + "\(host):\(port)" } public init?(from string: String) { @@ -88,3 +79,18 @@ extension Endpoint { } } } + +extension NWEndpoint.Host: CustomStringConvertible { + public var description: String { + switch self { + case .name(let hostname, _): + return hostname + case .ipv4(let address): + return "\(address)" + case .ipv6(let address): + return "\(address)" + @unknown default: + fatalError() + } + } +} From 7b35b4c604f1cf17b48aabf72526b15a2fdcc6ce Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Sun, 3 Dec 2023 22:10:37 -0800 Subject: [PATCH 10/39] Update RMF to support NetP waitlist matching (#582) Task/Issue URL: https://app.asana.com/0/0/1206027185389966/f iOS PR: duckduckgo/iOS#2209 macOS PR: duckduckgo/macos-browser#1912 What kind of version bump will this require?: Major Description: This PR updates RMF to support matching NetP waitlist users. --- .../JsonToRemoteMessageModelMapper.swift | 8 +++ .../Matchers/UserAttributeMatcher.swift | 25 +++++++- .../Model/JsonRemoteMessagingConfig.swift | 2 + .../Model/MatchingAttributes.swift | 63 +++++++++++++++++++ .../Model/RemoteMessageModel.swift | 2 + .../JsonToRemoteConfigModelMapperTests.swift | 28 ++++++++- .../Matchers/UserAttributeMatcherTests.swift | 28 ++++++++- .../RemoteMessagingConfigMatcherTests.swift | 16 +++-- .../RemoteMessagingConfigProcessorTests.swift | 8 ++- .../Resources/remote-messaging-config.json | 28 +++++++++ 10 files changed, 196 insertions(+), 12 deletions(-) diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift index d569b8a8f..ab3ce12a2 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift @@ -37,6 +37,8 @@ private enum AttributesKey: String, CaseIterable { case favorites case appTheme case daysSinceInstalled + case isNetPWaitlistUser + case daysSinceNetPEnabled func matchingAttribute(jsonMatchingAttribute: AnyDecodable) -> MatchingAttribute { switch self { @@ -55,6 +57,8 @@ private enum AttributesKey: String, CaseIterable { case .favorites: return FavoritesMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .appTheme: return AppThemeMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) case .daysSinceInstalled: return DaysSinceInstalledMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .isNetPWaitlistUser: return IsNetPWaitlistUserMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) + case .daysSinceNetPEnabled: return DaysSinceNetPEnabledMatchingAttribute(jsonMatchingAttribute: jsonMatchingAttribute) } } } @@ -158,6 +162,8 @@ struct JsonToRemoteMessageModelMapper { return .share(value: jsonAction.value, title: jsonAction.additionalParameters?["title"]) case .url: return .url(value: jsonAction.value) + case .surveyURL: + return .surveyURL(value: jsonAction.value) case .appStore: return .appStore case .dismiss: @@ -185,6 +191,8 @@ struct JsonToRemoteMessageModelMapper { return .macComputer case .newForMacAndWindows: return .newForMacAndWindows + case .vpnAnnounce: + return .vpnAnnounce case .none: return .announce } diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index 14ef91f00..d3ecea04a 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -30,6 +30,8 @@ public struct UserAttributeMatcher: AttributeMatcher { private let bookmarksCount: Int private let favoritesCount: Int private let isWidgetInstalled: Bool + private let isNetPWaitlistUser: Bool + private let daysSinceNetPEnabled: Int public init(statisticsStore: StatisticsStore, variantManager: VariantManager, @@ -37,7 +39,9 @@ public struct UserAttributeMatcher: AttributeMatcher { bookmarksCount: Int, favoritesCount: Int, appTheme: String, - isWidgetInstalled: Bool + isWidgetInstalled: Bool, + isNetPWaitlistUser: Bool, + daysSinceNetPEnabled: Int ) { self.statisticsStore = statisticsStore self.variantManager = variantManager @@ -46,9 +50,11 @@ public struct UserAttributeMatcher: AttributeMatcher { self.bookmarksCount = bookmarksCount self.favoritesCount = favoritesCount self.isWidgetInstalled = isWidgetInstalled + self.isNetPWaitlistUser = isNetPWaitlistUser + self.daysSinceNetPEnabled = daysSinceNetPEnabled } - // swiftlint:disable cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity function_body_length func evaluate(matchingAttribute: MatchingAttribute) -> EvaluationResult? { switch matchingAttribute { case let matchingAttribute as AppThemeMatchingAttribute: @@ -92,9 +98,22 @@ public struct UserAttributeMatcher: AttributeMatcher { } return BooleanMatchingAttribute(value).matches(value: isWidgetInstalled) + case let matchingAttribute as IsNetPWaitlistUserMatchingAttribute: + guard let value = matchingAttribute.value else { + return .fail + } + + return BooleanMatchingAttribute(value).matches(value: isNetPWaitlistUser) + case let matchingAttribute as DaysSinceNetPEnabledMatchingAttribute: + if matchingAttribute.value != MatchingAttributeDefaults.intDefaultValue { + return IntMatchingAttribute(matchingAttribute.value).matches(value: daysSinceNetPEnabled) + } else { + return RangeIntMatchingAttribute(min: matchingAttribute.min, max: matchingAttribute.max).matches(value: daysSinceNetPEnabled) + } default: + assertionFailure("Could not find matching attribute") return nil } } - // swiftlint:enable cyclomatic_complexity + } diff --git a/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift b/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift index 6be2d1def..16336dbc6 100644 --- a/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift +++ b/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift @@ -82,6 +82,7 @@ public enum RemoteMessageResponse { case url case appStore = "appstore" case dismiss + case surveyURL = "survey_url" } enum JsonPlaceholder: String, CaseIterable { @@ -91,6 +92,7 @@ public enum RemoteMessageResponse { case appUpdate = "AppUpdate" case macComputer = "MacComputer" case newForMacAndWindows = "NewForMacAndWindows" + case vpnAnnounce = "VPNAnnounce" } public enum StatusError: Error { diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index 9627dd6fa..1934ac896 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -596,6 +596,69 @@ struct RangeStringNumericMatchingAttribute: Equatable { } } +struct IsNetPWaitlistUserMatchingAttribute: MatchingAttribute, Equatable { + var value: Bool? + var fallback: Bool? + + init(jsonMatchingAttribute: AnyDecodable) { + guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } + + if let value = jsonMatchingAttribute[RuleAttributes.value] as? Bool { + self.value = value + } + if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { + self.fallback = fallback + } + } + + init(value: Bool?, fallback: Bool?) { + self.value = value + self.fallback = fallback + } + + static func == (lhs: IsNetPWaitlistUserMatchingAttribute, rhs: IsNetPWaitlistUserMatchingAttribute) -> Bool { + return lhs.value == rhs.value && lhs.fallback == rhs.fallback + } +} + +struct DaysSinceNetPEnabledMatchingAttribute: MatchingAttribute, Equatable { + var min: Int = MatchingAttributeDefaults.intDefaultValue + var max: Int = MatchingAttributeDefaults.intDefaultMaxValue + var value: Int = MatchingAttributeDefaults.intDefaultValue + var fallback: Bool? + + init(jsonMatchingAttribute: AnyDecodable) { + guard let jsonMatchingAttribute = jsonMatchingAttribute.value as? [String: Any] else { return } + + if let min = jsonMatchingAttribute[RuleAttributes.min] as? Int { + self.min = min + } + if let max = jsonMatchingAttribute[RuleAttributes.max] as? Int { + self.max = max + } + if let value = jsonMatchingAttribute[RuleAttributes.value] as? Int { + self.value = value + } + if let fallback = jsonMatchingAttribute[RuleAttributes.fallback] as? Bool { + self.fallback = fallback + } + } + + init(min: Int = MatchingAttributeDefaults.intDefaultValue, + max: Int = MatchingAttributeDefaults.intDefaultMaxValue, + value: Int = MatchingAttributeDefaults.intDefaultValue, + fallback: Bool?) { + self.min = min + self.max = max + self.value = value + self.fallback = fallback + } + + static func == (lhs: DaysSinceNetPEnabledMatchingAttribute, rhs: DaysSinceNetPEnabledMatchingAttribute) -> Bool { + return lhs.min == rhs.min && lhs.max == rhs.max && lhs.value == rhs.value && lhs.fallback == rhs.fallback + } +} + enum MatchingAttributeDefaults { static let intDefaultValue = -1 static let intDefaultMaxValue = Int.max diff --git a/Sources/RemoteMessaging/Model/RemoteMessageModel.swift b/Sources/RemoteMessaging/Model/RemoteMessageModel.swift index 09cf04094..d149929b4 100644 --- a/Sources/RemoteMessaging/Model/RemoteMessageModel.swift +++ b/Sources/RemoteMessaging/Model/RemoteMessageModel.swift @@ -103,6 +103,7 @@ public enum RemoteMessageModelType: Codable, Equatable { public enum RemoteAction: Codable, Equatable { case share(value: String, title: String?) case url(value: String) + case surveyURL(value: String) case appStore case dismiss } @@ -114,4 +115,5 @@ public enum RemotePlaceholder: String, Codable { case appUpdate = "RemoteMessageAppUpdate" case macComputer = "RemoteMessageMacComputer" case newForMacAndWindows = "RemoteMessageNewForMacAndWindows" + case vpnAnnounce = "RemoteMessageVPNAnnounce" } diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift index 3f5e78375..91486cd99 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift @@ -27,7 +27,7 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { func testWhenValidJsonParsedThenMessagesMappedIntoRemoteConfig() throws { let config = try decodeAndMapJson(fileName: "Resources/remote-messaging-config.json") - XCTAssertEqual(config.messages.count, 7) + XCTAssertEqual(config.messages.count, 8) XCTAssertEqual(config.messages[0], RemoteMessageModel( id: "8274589c-8aeb-4322-a737-3852911569e3", @@ -86,11 +86,24 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { exclusionRules: []) ) + XCTAssertEqual(config.messages[7], RemoteMessageModel( + id: "8E909844-C809-4543-AAFE-2C75DC285B3B", + content: .promoSingleAction( + titleText: "Survey Title", + descriptionText: "Survey Description", + placeholder: .vpnAnnounce, + actionText: "Survey Action", + action: .surveyURL(value: "https://duckduckgo.com/survey") + ), + matchingRules: [8], + exclusionRules: []) + ) + } func testWhenValidJsonParsedThenRulesMappedIntoRemoteConfig() throws { let config = try decodeAndMapJson(fileName: "Resources/remote-messaging-config.json") - XCTAssertTrue(config.rules.count == 3) + XCTAssertTrue(config.rules.count == 4) let rule5 = config.rules.filter { $0.key == 5 }.first XCTAssertNotNil(rule5) @@ -112,6 +125,17 @@ class JsonToRemoteConfigModelMapperTests: XCTestCase { attribs = rule7?.value.filter { $0 is WidgetAddedMatchingAttribute } XCTAssertEqual(attribs?.count, 1) XCTAssertEqual(attribs?.first as? WidgetAddedMatchingAttribute, WidgetAddedMatchingAttribute(value: false, fallback: nil)) + + let rule8 = config.rules.filter { $0.key == 8 }.first + XCTAssertNotNil(rule8) + XCTAssertTrue(rule8?.value.count == 2) + attribs = rule8?.value.filter { $0 is DaysSinceNetPEnabledMatchingAttribute } + XCTAssertEqual(attribs?.count, 1) + XCTAssertEqual(attribs?.first as? DaysSinceNetPEnabledMatchingAttribute, DaysSinceNetPEnabledMatchingAttribute(min: 5, fallback: nil)) + + attribs = rule8?.value.filter { $0 is IsNetPWaitlistUserMatchingAttribute } + XCTAssertEqual(attribs?.count, 1) + XCTAssertEqual(attribs?.first as? IsNetPWaitlistUserMatchingAttribute, IsNetPWaitlistUserMatchingAttribute(value: true, fallback: nil)) } func testWhenJsonMessagesHaveUnknownTypesThenMessagesNotMappedIntoConfig() throws { diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift index 5c0e1f19a..04398c249 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift @@ -53,7 +53,9 @@ class UserAttributeMatcherTests: XCTestCase { bookmarksCount: 44, favoritesCount: 88, appTheme: "default", - isWidgetInstalled: true) + isWidgetInstalled: true, + isNetPWaitlistUser: true, + daysSinceNetPEnabled: 3) } override func tearDownWithError() throws { @@ -181,6 +183,7 @@ class UserAttributeMatcherTests: XCTestCase { } // MARK: - EmailEnabled + func testWhenEmailEnabledMatchesThenReturnMatch() throws { XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: EmailEnabledMatchingAttribute(value: true, fallback: nil)), .match) @@ -192,6 +195,7 @@ class UserAttributeMatcherTests: XCTestCase { } // MARK: - WidgetAdded + func testWhenWidgetAddedMatchesThenReturnMatch() throws { XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: WidgetAddedMatchingAttribute(value: true, fallback: nil)), .match) @@ -202,4 +206,26 @@ class UserAttributeMatcherTests: XCTestCase { .fail) } + // MARK: - Network Protection Waitlist + + func testWhenIsNetPWaitlistUserMatchesThenReturnMatch() throws { + XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: IsNetPWaitlistUserMatchingAttribute(value: true, fallback: nil)), + .match) + } + + func testWhenIsNetPWaitlistUserDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: IsNetPWaitlistUserMatchingAttribute(value: false, fallback: nil)), + .fail) + } + + func testWhenDaysSinceNetPEnabledMatchesThenReturnMatch() throws { + XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: DaysSinceNetPEnabledMatchingAttribute(min: 1, fallback: nil)), + .match) + } + + func testWhenDaysSinceNetPEnabledDoesNotMatchThenReturnFail() throws { + XCTAssertEqual(userAttributeMatcher.evaluate(matchingAttribute: DaysSinceNetPEnabledMatchingAttribute(min: 7, fallback: nil)), + .fail) + } + } diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift index e98a7c950..9d80cc3c2 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift @@ -43,7 +43,9 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { bookmarksCount: 10, favoritesCount: 0, appTheme: "light", - isWidgetInstalled: false), + isWidgetInstalled: false, + isNetPWaitlistUser: false, + daysSinceNetPEnabled: -1), dismissedMessageIds: [] ) } @@ -111,7 +113,9 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { bookmarksCount: 0, favoritesCount: 0, appTheme: "light", - isWidgetInstalled: false), + isWidgetInstalled: false, + isNetPWaitlistUser: false, + daysSinceNetPEnabled: -1), dismissedMessageIds: []) let remoteConfig = RemoteConfigModel(messages: [mediumMessage(matchingRules: [1], exclusionRules: [2])], @@ -178,7 +182,9 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { bookmarksCount: 10, favoritesCount: 0, appTheme: "light", - isWidgetInstalled: false), + isWidgetInstalled: false, + isNetPWaitlistUser: false, + daysSinceNetPEnabled: -1), dismissedMessageIds: ["1"]) let remoteConfig = RemoteConfigModel(messages: [mediumMessage(matchingRules: [1], exclusionRules: []), @@ -206,7 +212,9 @@ class RemoteMessagingConfigMatcherTests: XCTestCase { bookmarksCount: 0, favoritesCount: 0, appTheme: "light", - isWidgetInstalled: false), + isWidgetInstalled: false, + isNetPWaitlistUser: false, + daysSinceNetPEnabled: -1), dismissedMessageIds: []) let remoteConfig = RemoteConfigModel(messages: [mediumMessage(matchingRules: [1, 2], exclusionRules: []), diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift index 59070b8af..593e8cf30 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift @@ -36,7 +36,9 @@ class RemoteMessagingConfigProcessorTests: XCTestCase { bookmarksCount: 0, favoritesCount: 0, appTheme: "light", - isWidgetInstalled: false), + isWidgetInstalled: false, + isNetPWaitlistUser: false, + daysSinceNetPEnabled: -1), dismissedMessageIds: [] ) @@ -62,7 +64,9 @@ class RemoteMessagingConfigProcessorTests: XCTestCase { bookmarksCount: 0, favoritesCount: 0, appTheme: "light", - isWidgetInstalled: false), + isWidgetInstalled: false, + isNetPWaitlistUser: false, + daysSinceNetPEnabled: -1), dismissedMessageIds: []) let processor = RemoteMessagingConfigProcessor(remoteMessagingConfigMatcher: remoteMessagingConfigMatcher) diff --git a/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json b/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json index 119d52d4f..fea7adc2d 100644 --- a/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json +++ b/Tests/BrowserServicesKitTests/Resources/remote-messaging-config.json @@ -133,6 +133,23 @@ "value": "" } } + }, + { + "id": "8E909844-C809-4543-AAFE-2C75DC285B3B", + "content": { + "messageType": "promo_single_action", + "titleText": "Survey Title", + "descriptionText": "Survey Description", + "placeholder": "VPNAnnounce", + "actionText": "Survey Action", + "action": { + "type": "survey_url", + "value": "https://duckduckgo.com/survey" + } + }, + "matchingRules": [ + 8 + ] } ], "rules": [ @@ -216,6 +233,17 @@ "value": false } } + }, + { + "id": 8, + "attributes": { + "isNetPWaitlistUser": { + "value": true + }, + "daysSinceNetPEnabled": { + "min": 5 + } + } } ] } From bb42a545ddcd2cd6209b3b668e43cfb2bf161bd0 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 4 Dec 2023 19:12:42 +0100 Subject: [PATCH 11/39] Fix SPM cache (#586) Task/Issue URL: https://app.asana.com/0/1203301625297703/1206090791724018/f Description: Limit cached directories to only SPM checkouts, leaving out actual build artifacts. --- .github/workflows/pr.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c01c1f27b..fb9d3160b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -48,7 +48,11 @@ jobs: if: env.cache_key_hash uses: actions/cache@v3 with: - path: .build + path: | + .build/artifacts + .build/checkouts + .build/repositories + .build/workspace-state.json key: ${{ runner.os }}-spm-${{ env.cache_key_hash }} restore-keys: | ${{ runner.os }}-spm- From 9a7c2f13f7769221809021371730bde3ab16e575 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 5 Dec 2023 16:09:06 +0100 Subject: [PATCH 12/39] Improve handling lists in Sync (#575) Task/Issue URL: https://app.asana.com/0/0/1205884549503153/f Description: Improve handling of lists in syncable models' payloads (which currently is limited to bookmarks folders and favorites folders children). This change updates how lists are sent to Sync in PATCH payloads by including current state as well as the set of removed and inserted items. To allow for this, last sync payload is kept for all bookmarks folders in lastChildrenPayloadReceivedFromSync property added on BookmarkEntity entity. If the payload is not available (e.g. first sync), the legacy mechanism is used (i.e. sending only the current contents of the list). The schema of a Sync response payload does not change so the implementation handles it too. --- Sources/Bookmarks/BookmarkEntity.swift | 23 +- .../.xccurrentversion | 2 +- .../BookmarksModel 5.xcdatamodel/contents | 27 ++ .../BookmarksTestsUtils/BookmarkTree.swift | 53 ++- .../Bookmarks/BookmarksProvider.swift | 12 +- .../internal/BookmarkEntity+Syncable.swift | 6 + .../internal/BookmarksResponseHandler.swift | 10 +- .../internal/SyncableBookmarkAdapter.swift | 43 ++- .../Credentials/CredentialsProvider.swift | 4 +- .../Settings/SettingsProvider.swift | 4 +- ...marksInitialSyncResponseHandlerTests.swift | 97 +++--- .../Bookmarks/BookmarksProviderTests.swift | 301 ++++++++++------ ...marksRegularSyncResponseHandlerTests.swift | 121 +++++-- .../SyncableBookmarkAdapterTests.swift | 325 ++++++++++++++++++ 14 files changed, 815 insertions(+), 213 deletions(-) create mode 100644 Sources/Bookmarks/BookmarksModel.xcdatamodeld/BookmarksModel 5.xcdatamodel/contents create mode 100644 Tests/SyncDataProvidersTests/Bookmarks/SyncableBookmarkAdapterTests.swift diff --git a/Sources/Bookmarks/BookmarkEntity.swift b/Sources/Bookmarks/BookmarkEntity.swift index 26b16a11a..23b2f6e0f 100644 --- a/Sources/Bookmarks/BookmarkEntity.swift +++ b/Sources/Bookmarks/BookmarkEntity.swift @@ -42,7 +42,7 @@ public class BookmarkEntity: NSManagedObject { } public static func isValidFavoritesFolderID(_ value: String) -> Bool { - FavoritesFolderID.allCases.contains { $0.rawValue == value } + Constants.favoriteFoldersIDs.contains(value) } public enum Error: Swift.Error { @@ -65,6 +65,7 @@ public class BookmarkEntity: NSManagedObject { @NSManaged public var url: String? @NSManaged public var uuid: String? @NSManaged public var children: NSOrderedSet? + @NSManaged public fileprivate(set) var lastChildrenPayloadReceivedFromSync: String? @NSManaged public fileprivate(set) var favoriteFolders: NSSet? @NSManaged public fileprivate(set) var favorites: NSOrderedSet? @NSManaged public var parent: BookmarkEntity? @@ -98,7 +99,10 @@ public class BookmarkEntity: NSManagedObject { return } let changedKeys = changedValues().keys - guard !changedKeys.isEmpty, !changedKeys.contains(NSStringFromSelector(#selector(getter: modifiedAt))) else { + guard !changedKeys.isEmpty, + !changedKeys.contains(NSStringFromSelector(#selector(getter: modifiedAt))), + Array(changedKeys) != [NSStringFromSelector(#selector(getter: lastChildrenPayloadReceivedFromSync))] + else { return } if isInserted, let uuid, uuid == Constants.rootFolderID || Self.isValidFavoritesFolderID(uuid) { @@ -147,6 +151,21 @@ public class BookmarkEntity: NSManagedObject { return favoriteFolders.flatMap(Set.init) ?? [] } + public var lastChildrenArrayReceivedFromSync: [String]? { + get { + guard let lastChildrenPayloadReceivedFromSync else { + return nil + } + guard !lastChildrenPayloadReceivedFromSync.isEmpty else { + return [] + } + return lastChildrenPayloadReceivedFromSync.components(separatedBy: ",") + } + set { + lastChildrenPayloadReceivedFromSync = newValue?.filter({ !$0.isEmpty }).joined(separator: ",") + } + } + public static func makeFolder(title: String, parent: BookmarkEntity, insertAtBeginning: Bool = false, diff --git a/Sources/Bookmarks/BookmarksModel.xcdatamodeld/.xccurrentversion b/Sources/Bookmarks/BookmarksModel.xcdatamodeld/.xccurrentversion index 9ebe855d9..6801520c4 100644 --- a/Sources/Bookmarks/BookmarksModel.xcdatamodeld/.xccurrentversion +++ b/Sources/Bookmarks/BookmarksModel.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - BookmarksModel 4.xcdatamodel + BookmarksModel 5.xcdatamodel diff --git a/Sources/Bookmarks/BookmarksModel.xcdatamodeld/BookmarksModel 5.xcdatamodel/contents b/Sources/Bookmarks/BookmarksModel.xcdatamodeld/BookmarksModel 5.xcdatamodel/contents new file mode 100644 index 000000000..f96388c5a --- /dev/null +++ b/Sources/Bookmarks/BookmarksModel.xcdatamodeld/BookmarksModel 5.xcdatamodel/contents @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/BookmarksTestsUtils/BookmarkTree.swift b/Sources/BookmarksTestsUtils/BookmarkTree.swift index e50347c4d..dd0b538e4 100644 --- a/Sources/BookmarksTestsUtils/BookmarkTree.swift +++ b/Sources/BookmarksTestsUtils/BookmarkTree.swift @@ -61,13 +61,13 @@ public struct ModifiedAtConstraint { public enum BookmarkTreeNode { case bookmark(id: String, name: String?, url: String?, favoritedOn: [FavoritesFolderID], modifiedAt: Date?, isDeleted: Bool, isOrphaned: Bool, modifiedAtConstraint: ModifiedAtConstraint?) - case folder(id: String, name: String?, children: [BookmarkTreeNode], modifiedAt: Date?, isDeleted: Bool, isOrphaned: Bool, modifiedAtConstraint: ModifiedAtConstraint?) + case folder(id: String, name: String?, children: [BookmarkTreeNode], modifiedAt: Date?, isDeleted: Bool, isOrphaned: Bool, lastChildrenArrayReceivedFromSync: [String]?, modifiedAtConstraint: ModifiedAtConstraint?) public var id: String { switch self { case .bookmark(let id, _, _, _, _, _, _, _): return id - case .folder(let id, _, _, _, _, _, _): + case .folder(let id, _, _, _, _, _, _, _): return id } } @@ -76,7 +76,7 @@ public enum BookmarkTreeNode { switch self { case .bookmark(_, let name, _, _, _, _, _, _): return name - case .folder(_, let name, _, _, _, _, _): + case .folder(_, let name, _, _, _, _, _, _): return name } } @@ -85,7 +85,7 @@ public enum BookmarkTreeNode { switch self { case .bookmark(_, _, _, _, let modifiedAt, _, _, _): return modifiedAt - case .folder(_, _, _, let modifiedAt, _, _, _): + case .folder(_, _, _, let modifiedAt, _, _, _, _): return modifiedAt } } @@ -94,7 +94,7 @@ public enum BookmarkTreeNode { switch self { case .bookmark(_, _, _, _, _, let isDeleted, _, _): return isDeleted - case .folder(_, _, _, _, let isDeleted, _, _): + case .folder(_, _, _, _, let isDeleted, _, _, _): return isDeleted } } @@ -103,16 +103,25 @@ public enum BookmarkTreeNode { switch self { case .bookmark(_, _, _, _, _, _, let isOrphaned, _): return isOrphaned - case .folder(_, _, _, _, _, let isOrphaned, _): + case .folder(_, _, _, _, _, let isOrphaned, _, _): return isOrphaned } } + public var lastChildrenArrayReceivedFromSync: [String]? { + switch self { + case .bookmark: + return nil + case .folder(_, _, _, _, _, _, let lastChildrenArrayReceivedFromSync, _): + return lastChildrenArrayReceivedFromSync + } + } + public var modifiedAtConstraint: ModifiedAtConstraint? { switch self { case .bookmark(_, _, _, _, _, _, _, let modifiedAtConstraint): return modifiedAtConstraint - case .folder(_, _, _, _, _, _, let modifiedAtConstraint): + case .folder(_, _, _, _, _, _, _, let modifiedAtConstraint): return modifiedAtConstraint } } @@ -155,24 +164,26 @@ public struct Folder: BookmarkTreeNodeConvertible { var isDeleted: Bool var isOrphaned: Bool var modifiedAtConstraint: ModifiedAtConstraint? + var lastChildrenArrayReceivedFromSync: [String]? var children: [BookmarkTreeNode] - public init(_ name: String? = nil, id: String? = nil, modifiedAt: Date? = nil, isDeleted: Bool = false, isOrphaned: Bool = false, @BookmarkTreeBuilder children: () -> [BookmarkTreeNode] = { [] }) { - self.init(name, id: id, modifiedAt: modifiedAt, isDeleted: isDeleted, isOrphaned: isOrphaned, modifiedAtConstraint: nil, children: children) + public init(_ name: String? = nil, id: String? = nil, modifiedAt: Date? = nil, isDeleted: Bool = false, isOrphaned: Bool = false, lastChildrenArrayReceivedFromSync: [String]? = nil, @BookmarkTreeBuilder children: () -> [BookmarkTreeNode] = { [] }) { + self.init(name, id: id, modifiedAt: modifiedAt, isDeleted: isDeleted, isOrphaned: isOrphaned, modifiedAtConstraint: nil, lastChildrenArrayReceivedFromSync: lastChildrenArrayReceivedFromSync, children: children) } - public init(_ name: String? = nil, id: String? = nil, modifiedAt: Date? = nil, isDeleted: Bool = false, isOrphaned: Bool = false, modifiedAtConstraint: ModifiedAtConstraint? = nil, @BookmarkTreeBuilder children: () -> [BookmarkTreeNode] = { [] }) { + public init(_ name: String? = nil, id: String? = nil, modifiedAt: Date? = nil, isDeleted: Bool = false, isOrphaned: Bool = false, modifiedAtConstraint: ModifiedAtConstraint? = nil, lastChildrenArrayReceivedFromSync: [String]? = nil, @BookmarkTreeBuilder children: () -> [BookmarkTreeNode] = { [] }) { self.id = id ?? UUID().uuidString self.name = name ?? id self.modifiedAt = modifiedAt self.isDeleted = isDeleted self.isOrphaned = isOrphaned + self.lastChildrenArrayReceivedFromSync = lastChildrenArrayReceivedFromSync self.modifiedAtConstraint = modifiedAtConstraint self.children = children() } public func asBookmarkTreeNode() -> BookmarkTreeNode { - .folder(id: id, name: name, children: children, modifiedAt: modifiedAt, isDeleted: isDeleted, isOrphaned: isOrphaned, modifiedAtConstraint: modifiedAtConstraint) + .folder(id: id, name: name, children: children, modifiedAt: modifiedAt, isDeleted: isDeleted, isOrphaned: isOrphaned, lastChildrenArrayReceivedFromSync: lastChildrenArrayReceivedFromSync, modifiedAtConstraint: modifiedAtConstraint) } } @@ -186,9 +197,10 @@ public struct BookmarkTreeBuilder { public struct BookmarkTree { - public init(modifiedAt: Date? = nil, modifiedAtConstraint: ModifiedAtConstraint? = nil, @BookmarkTreeBuilder builder: () -> [BookmarkTreeNode]) { + public init(modifiedAt: Date? = nil, modifiedAtConstraint: ModifiedAtConstraint? = nil, lastChildrenArrayReceivedFromSync: [String]? = nil, @BookmarkTreeBuilder builder: () -> [BookmarkTreeNode]) { self.modifiedAt = modifiedAt self.modifiedAtConstraint = modifiedAtConstraint + self.lastChildrenArrayReceivedFromSync = lastChildrenArrayReceivedFromSync self.bookmarkTreeNodes = builder() } @@ -203,6 +215,9 @@ public struct BookmarkTree { public func createEntitiesForCheckingModifiedAt(in context: NSManagedObjectContext) -> (BookmarkEntity, [BookmarkEntity], [String: ModifiedAtConstraint]) { let rootFolder = BookmarkUtils.fetchRootFolder(context)! rootFolder.modifiedAt = modifiedAt + if let lastChildrenArrayReceivedFromSync { + rootFolder.lastChildrenArrayReceivedFromSync = lastChildrenArrayReceivedFromSync + } let favoritesFolders = BookmarkUtils.fetchFavoritesFolders(withUUIDs: Set(FavoritesFolderID.allCases.map(\.rawValue)), in: context) var orphans = [BookmarkEntity]() var modifiedAtConstraints = [String: ModifiedAtConstraint]() @@ -224,6 +239,7 @@ public struct BookmarkTree { // swiftlint:enable large_tuple let modifiedAt: Date? + let lastChildrenArrayReceivedFromSync: [String]? let modifiedAtConstraint: ModifiedAtConstraint? let bookmarkTreeNodes: [BookmarkTreeNode] } @@ -274,7 +290,7 @@ public extension BookmarkEntity { if !isOrphaned { bookmarkEntity.parent = parent } - case .folder(let id, let name, let children, let modifiedAt, let isDeleted, let isOrphaned, let modifiedAtConstraint): + case .folder(let id, let name, let children, let modifiedAt, let isDeleted, let isOrphaned, let lastChildrenArrayReceivedFromSync, let modifiedAtConstraint): let bookmarkEntity = BookmarkEntity(context: context) if entity == nil { entity = bookmarkEntity @@ -290,6 +306,9 @@ public extension BookmarkEntity { if !isOrphaned { bookmarkEntity.parent = parent } + if let lastChildrenArrayReceivedFromSync { + bookmarkEntity.lastChildrenArrayReceivedFromSync = lastChildrenArrayReceivedFromSync + } parents.append(bookmarkEntity) queues.append(children) } @@ -301,7 +320,7 @@ public extension BookmarkEntity { } public extension XCTestCase { - func assertEquivalent(withTimestamps: Bool = true, _ bookmarkEntity: BookmarkEntity, _ tree: BookmarkTree, file: StaticString = #file, line: UInt = #line) { + func assertEquivalent(withTimestamps: Bool = true, withLastChildrenArrayReceivedFromSync: Bool = true, _ bookmarkEntity: BookmarkEntity, _ tree: BookmarkTree, file: StaticString = #file, line: UInt = #line) { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.persistentStoreCoordinator = bookmarkEntity.managedObjectContext?.persistentStoreCoordinator @@ -325,6 +344,9 @@ public extension XCTestCase { let thisFolder = bookmarkEntity XCTAssertEqual(expectedRootFolder.uuid, thisFolder.uuid, "root folder uuid mismatch", file: file, line: line) + if withLastChildrenArrayReceivedFromSync { + XCTAssertEqual(expectedRootFolder.lastChildrenArrayReceivedFromSync, thisFolder.lastChildrenArrayReceivedFromSync, "root folder lastChildrenArrayReceivedFromSync mismatch", file: file, line: line) + } var expectedTreeQueue: [BookmarkEntity] = [[expectedRootFolder], expectedOrphans].flatMap { $0 } var thisTreeQueue: [BookmarkEntity] = [[thisFolder], orphans].flatMap { $0 } @@ -355,6 +377,9 @@ public extension XCTestCase { } if expectedNode.isFolder { + if withLastChildrenArrayReceivedFromSync { + XCTAssertEqual(expectedNode.lastChildrenArrayReceivedFromSync, thisNode.lastChildrenArrayReceivedFromSync, "lastChildrenArrayReceivedFromSync mismatch for \(thisUUID)", file: file, line: line) + } XCTAssertEqual(expectedNode.childrenArray.count, thisNode.childrenArray.count, "children count mismatch for \(thisUUID)", file: file, line: line) expectedTreeQueue.append(contentsOf: expectedNode.childrenArray) thisTreeQueue.append(contentsOf: thisNode.childrenArray) diff --git a/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift b/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift index b9ea91f87..5e5bd8342 100644 --- a/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift +++ b/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift @@ -58,6 +58,7 @@ public final class BookmarksProvider: DataProvider { let bookmarks = (try? context.fetch(fetchRequest)) ?? [] for bookmark in bookmarks { bookmark.modifiedAt = Date() + bookmark.lastChildrenArrayReceivedFromSync = nil } do { @@ -163,10 +164,17 @@ public final class BookmarksProvider: DataProvider { if let modifiedAt = bookmark.modifiedAt, modifiedAt > clientTimestamp { continue } - let isLocalChangeRejectedBySync: Bool = bookmark.uuid.flatMap { receivedUUIDs.contains($0) } == true - if bookmark.isPendingDeletion, !isLocalChangeRejectedBySync { + let hasNewerVersionOnServer: Bool = bookmark.uuid.flatMap { receivedUUIDs.contains($0) } == true + if bookmark.isPendingDeletion, !hasNewerVersionOnServer { context.delete(bookmark) } else { + if !hasNewerVersionOnServer, bookmark.isFolder { + if bookmark.uuid.flatMap(BookmarkEntity.isValidFavoritesFolderID) == true { + bookmark.updateLastChildrenSyncPayload(with: bookmark.favoritesArray.compactMap(\.uuid)) + } else { + bookmark.updateLastChildrenSyncPayload(with: bookmark.childrenArray.compactMap(\.uuid)) + } + } bookmark.modifiedAt = nil if let uuid = bookmark.uuid { idsOfItemsToClearModifiedAt.insert(uuid) diff --git a/Sources/SyncDataProviders/Bookmarks/internal/BookmarkEntity+Syncable.swift b/Sources/SyncDataProviders/Bookmarks/internal/BookmarkEntity+Syncable.swift index 0800dd372..4737d02df 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/BookmarkEntity+Syncable.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/BookmarkEntity+Syncable.swift @@ -125,6 +125,12 @@ extension BookmarkEntity { } } } + + func updateLastChildrenSyncPayload(with uuids: [String]) { + if isFolder { + lastChildrenArrayReceivedFromSync = uuids + } + } } extension Array where Element == BookmarkEntity { diff --git a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift index 584402b0f..b7bb586e6 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift @@ -46,7 +46,7 @@ final class BookmarksResponseHandler { init(received: [Syncable], clientTimestamp: Date? = nil, context: NSManagedObjectContext, crypter: Crypting, deduplicateEntities: Bool) throws { self.clientTimestamp = clientTimestamp - self.received = received.map(SyncableBookmarkAdapter.init) + self.received = received.map { SyncableBookmarkAdapter(syncable: $0) } self.context = context self.shouldDeduplicateEntities = deduplicateEntities @@ -139,6 +139,8 @@ final class BookmarksResponseHandler { bookmark.addToFavorites(favoritesRoot: favoritesFolder) } } + + favoritesFolder.updateLastChildrenSyncPayload(with: favoritesUUIDs) } } @@ -221,6 +223,8 @@ final class BookmarksResponseHandler { parent?.addToChildren(deduplicatedEntity) } + deduplicatedEntity.updateLastChildrenSyncPayload(with: syncable.children) + } else if let existingEntity = entitiesByUUID[syncableUUID] { let isModifiedAfterSyncTimestamp: Bool = { guard let clientTimestamp, let modifiedAt = existingEntity.modifiedAt else { @@ -237,6 +241,8 @@ final class BookmarksResponseHandler { parent?.addToChildren(existingEntity) } + existingEntity.updateLastChildrenSyncPayload(with: syncable.children) + } else if !syncable.isDeleted { assert(syncable.uuid != BookmarkEntity.Constants.rootFolderID, "Trying to make another root folder") @@ -245,6 +251,8 @@ final class BookmarksResponseHandler { parent?.addToChildren(newEntity) try updateEntity(newEntity, with: syncable) entitiesByUUID[syncableUUID] = newEntity + + newEntity.updateLastChildrenSyncPayload(with: syncable.children) } } diff --git a/Sources/SyncDataProviders/Bookmarks/internal/SyncableBookmarkAdapter.swift b/Sources/SyncDataProviders/Bookmarks/internal/SyncableBookmarkAdapter.swift index 40a1df9ff..cd3c46c6a 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/SyncableBookmarkAdapter.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/SyncableBookmarkAdapter.swift @@ -51,10 +51,20 @@ struct SyncableBookmarkAdapter { } var children: [String] { - guard let folder = syncable.payload["folder"] as? [String: Any], let folderChildren = folder["children"] as? [String] else { + guard let folder = syncable.payload["folder"] as? [String: Any] else { return [] } - return folderChildren + + if let folderChildrenDictionary = folder["children"] as? [String: Any], + let currentChildren = folderChildrenDictionary["current"] as? [String] { + + return currentChildren + + } else if let children = folder["children"] as? [String] { + return children + } + + return [] } } @@ -79,15 +89,28 @@ extension Syncable { payload["title"] = try encrypt(title) } if bookmark.isFolder { - if BookmarkEntity.Constants.favoriteFoldersIDs.contains(uuid) { - payload["folder"] = [ - "children": bookmark.favoritesArray.map(\.uuid) - ] - } else { - payload["folder"] = [ - "children": bookmark.childrenArray.map(\.uuid) - ] + let children: [String] = { + if BookmarkEntity.Constants.favoriteFoldersIDs.contains(uuid) { + return bookmark.favoritesArray.compactMap(\.uuid) + } + return bookmark.childrenArray.compactMap(\.uuid) + }() + + let lastReceivedChildren = bookmark.lastChildrenArrayReceivedFromSync ?? [] + let insert = Array(Set(children).subtracting(lastReceivedChildren)) + let remove = Array(Set(lastReceivedChildren).subtracting(children)) + + var childrenDict = [String: [String]]() + childrenDict["current"] = children + if !insert.isEmpty { + childrenDict["insert"] = insert + } + if !remove.isEmpty { + childrenDict["remove"] = remove } + + payload["folder"] = ["children": childrenDict] + } else if let url = bookmark.url { payload["page"] = ["url": try encrypt(url)] } diff --git a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift index 2b8019594..0eaa1b999 100644 --- a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift +++ b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift @@ -201,8 +201,8 @@ public final class CredentialsProvider: DataProvider { if let modifiedAt = metadataRecord.lastModified, modifiedAt.compareWithMillisecondPrecision(to: clientTimestamp) == .orderedDescending { continue } - let isLocalChangeRejectedBySync: Bool = receivedUUIDs.contains(metadataRecord.uuid) - if metadataRecord.objectId == nil, !isLocalChangeRejectedBySync { + let hasNewerVersionOnServer: Bool = receivedUUIDs.contains(metadataRecord.uuid) + if metadataRecord.objectId == nil, !hasNewerVersionOnServer { try metadataRecord.delete(database) } else { idsOfItemsToClearModifiedAt.insert(metadataRecord.uuid) diff --git a/Sources/SyncDataProviders/Settings/SettingsProvider.swift b/Sources/SyncDataProviders/Settings/SettingsProvider.swift index f097365ec..05c3dcb43 100644 --- a/Sources/SyncDataProviders/Settings/SettingsProvider.swift +++ b/Sources/SyncDataProviders/Settings/SettingsProvider.swift @@ -291,9 +291,9 @@ public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { if let lastModified = metadata.lastModified, lastModified > clientTimestamp { continue } - let isLocalChangeRejectedBySync: Bool = receivedKeys.contains(metadata.key) + let hasNewerVersionOnServer: Bool = receivedKeys.contains(metadata.key) let isPendingDeletion = originalValues[setting] == nil - if isPendingDeletion, !isLocalChangeRejectedBySync { + if isPendingDeletion, !hasNewerVersionOnServer { try handler.setValue(nil) } else { idsOfItemsToClearModifiedAt.insert(metadata.key) diff --git a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksInitialSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksInitialSyncResponseHandlerTests.swift index 47e7e3b60..bf33e3fc6 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksInitialSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksInitialSyncResponseHandlerTests.swift @@ -38,7 +38,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase let received: [Syncable] = [.rootFolder(children: ["2", "1"])] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["2", "1"]) { Bookmark(id: "2") Bookmark(id: "1") }) @@ -58,7 +58,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "3"]) { Bookmark(id: "1") Bookmark(id: "2") Bookmark(id: "3") @@ -79,7 +79,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "3"]) { Bookmark(id: "1") Bookmark(id: "2") Bookmark(id: "3") @@ -100,7 +100,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3"]) { Bookmark(id: "1") Bookmark(id: "2") Bookmark(id: "3") @@ -124,8 +124,8 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder("Folder", id: "1") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1"]) { + Folder("Folder", id: "1", lastChildrenArrayReceivedFromSync: ["4"]) { Bookmark(id: "2") Bookmark(id: "3") Bookmark(id: "4") @@ -150,7 +150,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "3"]) { Bookmark(id: "1", favoritedOn: [.mobile, .unified]) Bookmark(id: "2", favoritedOn: [.mobile, .unified]) Bookmark(id: "3", favoritedOn: [.desktop, .unified]) @@ -173,7 +173,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3"]) { Bookmark(id: "1", favoritedOn: [.mobile, .unified]) Bookmark(id: "4", favoritedOn: [.mobile, .unified]) Bookmark(id: "3", favoritedOn: [.mobile, .unified]) @@ -196,7 +196,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3", "remote2", "4"]) { Bookmark(id: "1") Bookmark(id: "3") Bookmark("2", id: "remote2") @@ -218,7 +218,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["2"]) { Bookmark(id: "2") }) } @@ -241,7 +241,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2"]) { Bookmark(id: "1") Bookmark(id: "2") }) @@ -264,7 +264,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3", "remote2", "4"]) { Bookmark(id: "3") Bookmark("2", id: "remote2") Bookmark(id: "4") @@ -286,7 +286,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(rootFolder, BookmarkTree { + assertEquivalent(withLastChildrenArrayReceivedFromSync: true, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["2"]) { Bookmark("name", id: "2", url: "url") }) } @@ -313,10 +313,10 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder(id: "1") { - Folder(id: "2") { - Folder(id: "3") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1"]) { + Folder(id: "1", lastChildrenArrayReceivedFromSync: ["2"]) { + Folder(id: "2", lastChildrenArrayReceivedFromSync: ["3"]) { + Folder(id: "3", lastChildrenArrayReceivedFromSync: ["5"]) { Bookmark("name", id: "5", url: "url") } } @@ -341,9 +341,9 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder(id: "1") - Folder(id: "2") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2"]) { + Folder(id: "1", lastChildrenArrayReceivedFromSync: []) + Folder(id: "2", lastChildrenArrayReceivedFromSync: ["3"]) { Bookmark("name", id: "3", url: "url") } }) @@ -364,7 +364,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["2"]) { Bookmark(id: "1", favoritedOn: [.mobile, .unified]) Bookmark(id: "2", favoritedOn: [.mobile, .unified]) }) @@ -374,6 +374,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase favoritesFolder = BookmarkUtils.fetchFavoritesFolder(withUUID: FavoritesFolderID.unified.rawValue, in: context) } XCTAssertNotNil(favoritesFolder.modifiedAt) + XCTAssertEqual(favoritesFolder.lastChildrenArrayReceivedFromSync, ["2"]) } func testThatFoldersWithTheSameNameAndParentAreDeduplicated() async throws { @@ -398,10 +399,10 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder("1st level", id: "remote1") { - Folder("2nd level", id: "remote2") { - Folder("Duplicated folder", id: "remote5") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["remote1"]) { + Folder("1st level", id: "remote1", lastChildrenArrayReceivedFromSync: ["remote2"]) { + Folder("2nd level", id: "remote2", lastChildrenArrayReceivedFromSync: ["remote5"]) { + Folder("Duplicated folder", id: "remote5", lastChildrenArrayReceivedFromSync: ["remote6"]) { Bookmark(id: "local4") Bookmark(id: "remote6") } @@ -440,10 +441,10 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder("1", id: "remote1") { - Folder("2", id: "remote2") { - Folder("Duplicated folder", id: "remote9") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["remote1"]) { + Folder("1", id: "remote1", lastChildrenArrayReceivedFromSync: ["remote2"]) { + Folder("2", id: "remote2", lastChildrenArrayReceivedFromSync: ["remote9"]) { + Folder("Duplicated folder", id: "remote9", lastChildrenArrayReceivedFromSync: ["remote10", "remote11", "remote12"]) { Folder("4", id: "local4") { Bookmark("5", id: "local5") } @@ -452,7 +453,7 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase Bookmark("8", id: "local8") Bookmark(id: "remote10") Bookmark(id: "remote11") - Folder(id: "remote12") { + Folder(id: "remote12", lastChildrenArrayReceivedFromSync: ["remote13"]) { Bookmark(id: "remote13") } } @@ -476,8 +477,8 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder("1", id: "11") + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["11", "12"]) { + Folder("1", id: "11", lastChildrenArrayReceivedFromSync: []) Bookmark("2", id: "12") }) } @@ -537,16 +538,16 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder("01", id: "101") { - Folder("02", id: "102") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["101", "115", "116", "119"]) { + Folder("01", id: "101", lastChildrenArrayReceivedFromSync: ["102", "104", "105", "114"]) { + Folder("02", id: "102", lastChildrenArrayReceivedFromSync: ["103"]) { Bookmark("03", id: "103") } Bookmark("04", id: "104") - Folder("05", id: "105") { + Folder("05", id: "105", lastChildrenArrayReceivedFromSync: ["106", "107", "112", "113"]) { Bookmark("06", id: "106") - Folder("07", id: "107") { - Folder("08", id: "108") + Folder("07", id: "107", lastChildrenArrayReceivedFromSync: ["108", "109", "110", "111"]) { + Folder("08", id: "108", lastChildrenArrayReceivedFromSync: []) Bookmark("09", id: "109") Bookmark("10", id: "110") Bookmark("11", id: "111") @@ -557,8 +558,8 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase Bookmark("14", id: "114") } Bookmark("15", id: "115") - Folder("16", id: "116") { - Folder("17", id: "117") { + Folder("16", id: "116", lastChildrenArrayReceivedFromSync: ["117"]) { + Folder("17", id: "117", lastChildrenArrayReceivedFromSync: ["118"]) { Bookmark("18", id: "118") } } @@ -583,12 +584,12 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder(id: "1") - Folder(id: "2") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2"]) { + Folder(id: "1", lastChildrenArrayReceivedFromSync: []) + Folder(id: "2", lastChildrenArrayReceivedFromSync: ["3"]) { Bookmark("name", id: "3", url: "url") } - Folder(id: "4", isOrphaned: true) { + Folder(id: "4", isOrphaned: true, lastChildrenArrayReceivedFromSync: ["5"]) { Bookmark(id: "5") } }) @@ -609,12 +610,12 @@ final class BookmarksInitialSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleInitialSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder(id: "1") - Folder(id: "2") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2"]) { + Folder(id: "1", lastChildrenArrayReceivedFromSync: []) + Folder(id: "2", lastChildrenArrayReceivedFromSync: ["3"]) { Bookmark("name", id: "3", url: "url") } - Folder(id: "4", isOrphaned: true) { + Folder(id: "4", isOrphaned: true, lastChildrenArrayReceivedFromSync: ["5"]) { Bookmark(id: "5") } }) diff --git a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift index d81d76c3f..82c80bd3a 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift @@ -93,7 +93,8 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { } try provider.prepareForFirstSync() - let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter).map(SyncableBookmarkAdapter.init) + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) XCTAssertEqual( Set(changedObjects.compactMap(\.uuid)), @@ -128,7 +129,8 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { try! context.save() } - let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter).map(SyncableBookmarkAdapter.init) + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) XCTAssertEqual( Set(changedObjects.compactMap(\.uuid)), @@ -164,7 +166,8 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { try! context.save() } - let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter).map(SyncableBookmarkAdapter.init) + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) let changedFolder = try XCTUnwrap(changedObjects.first(where: { $0.uuid == "2"})) XCTAssertEqual(Set(changedObjects.compactMap(\.uuid)), Set(["2", "4"])) @@ -198,7 +201,7 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { context.refreshAllObjects() let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "5", "6"]) { Bookmark("Bookmark 1", id: "1") Bookmark("Bookmark 5", id: "5") Bookmark("Bookmark 6", id: "6") @@ -230,8 +233,8 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { context.performAndWait { context.refreshAllObjects() let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Folder(id: "1") { + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "4", "5"]) { + Folder(id: "1", lastChildrenArrayReceivedFromSync: ["2", "3"]) { Bookmark(id: "2") Bookmark(id: "3") } @@ -264,7 +267,7 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { context.performAndWait { context.refreshAllObjects() let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["2"]) { Bookmark("test", id: "2", url: "test") }) } @@ -296,7 +299,7 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { context.performAndWait { context.refreshAllObjects() let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree(modifiedAtConstraint: .notNil()) { + assertEquivalent(rootFolder, BookmarkTree(modifiedAtConstraint: .notNil(), lastChildrenArrayReceivedFromSync: ["3", "4"]) { Bookmark(id: "1", modifiedAtConstraint: .notNil()) Bookmark(id: "2", modifiedAtConstraint: .notNil()) Bookmark(id: "3", modifiedAtConstraint: .nil()) @@ -334,8 +337,8 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { context.performAndWait { context.refreshAllObjects() let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree(modifiedAtConstraint: .nil()) { - Folder("Folder", id: "4", modifiedAtConstraint: .greaterThan(clientTimestamp)) { + assertEquivalent(rootFolder, BookmarkTree(modifiedAtConstraint: .nil(), lastChildrenArrayReceivedFromSync: ["4"]) { + Folder("Folder", id: "4", modifiedAtConstraint: .greaterThan(clientTimestamp), lastChildrenArrayReceivedFromSync: ["5", "6"]) { Bookmark(id: "2", modifiedAtConstraint: .notNil()) Bookmark(id: "3", modifiedAtConstraint: .notNil()) Bookmark(id: "5", modifiedAtConstraint: .nil()) @@ -366,18 +369,13 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { } let clientTimestamp = Date() - try await provider.handleInitialSyncResponse(received: received, clientTimestamp: clientTimestamp, serverTimestamp: "1234", crypter: crypter) - - context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Folder("Folder", id: "4") { - Bookmark(id: "5") - Bookmark(id: "6") - } - }) - } + let rootFolder = try await handleInitialSyncResponse(received: received, clientTimestamp: clientTimestamp, serverTimestamp: "1234", in: context) + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["4"]) { + Folder("Folder", id: "4", lastChildrenArrayReceivedFromSync: ["5", "6"]) { + Bookmark(id: "5") + Bookmark(id: "6") + } + }) } func testWhenThereIsMergeConflictDuringInitialSyncThenSyncResponseHandlingIsRetried() async throws { @@ -412,22 +410,18 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { bookmarkModificationDate = rootFolder.childrenArray.first!.modifiedAt } } - try await provider.handleInitialSyncResponse(received: received, clientTimestamp: Date(), serverTimestamp: "1234", crypter: crypter) + let rootFolder = try await handleInitialSyncResponse(received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) XCTAssertEqual(willSaveCallCount, 2) - context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Bookmark("test-local", id: "1", modifiedAt: bookmarkModificationDate) - }) - } + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1"]) { + Bookmark("test-local", id: "1", modifiedAt: bookmarkModificationDate) + }) } // MARK: - Regular Sync - func testWhenObjectDeleteIsSentAndTheSameObjectUpdateIsReceivedThenObjectIsNotDeleted() async throws { + func testWhenObjectDeleteIsSentAndTheSameObjectDeleteIsReceivedThenObjectIsDeleted() async throws { let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) let bookmarkTree = BookmarkTree { @@ -435,8 +429,8 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { } let received: [Syncable] = [ - .rootFolder(children: ["1"]), - .bookmark("test2", id: "1") + .rootFolder(children: []), + .bookmark("test2", id: "1", isDeleted: true) ] context.performAndWait { @@ -447,15 +441,34 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { let sent = try await provider.fetchChangedObjects(encryptedUsing: crypter) - try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", crypter: crypter) + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: []) {}) + } + + func testWhenObjectDeleteIsSentAndTheSameObjectUpdateIsReceivedThenObjectIsNotDeleted() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark("test", id: "1", isDeleted: true) + } + + let received: [Syncable] = [ + .rootFolder(children: ["1"]), + .bookmark("test2", id: "1") + ] context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Bookmark("test2", id: "1") - }) + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() } + + let sent = try await provider.fetchChangedObjects(encryptedUsing: crypter) + + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1"]) { + Bookmark("test2", id: "1") + }) } func testWhenObjectDeleteIsSentAndTheSameObjectUpdateIsReceivedWithoutParentFolderThenObjectIsNotDeleted() async throws { @@ -477,15 +490,10 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { let sent = try await provider.fetchChangedObjects(encryptedUsing: crypter) - try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", crypter: crypter) - - context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Bookmark("test2", id: "1") - }) - } + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(withLastChildrenArrayReceivedFromSync: false, rootFolder, BookmarkTree { + Bookmark("test2", id: "1") + }) } func testWhenObjectDeleteIsSentAndTheSameObjectUpdateIsReceivedThenObjectIsNotDeletedAndIsNotMovedWithinFolder() async throws { @@ -508,16 +516,37 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { let sent = try await provider.fetchChangedObjects(encryptedUsing: crypter) - try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", crypter: crypter) + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(withLastChildrenArrayReceivedFromSync: false, rootFolder, BookmarkTree { + Bookmark("test2", id: "1") + Bookmark(id: "2") + }) + } + + func testWhenFolderUpdateIsSentAndTheSameFolderUpdateIsReceivedThenServerVersionIsApplied() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark("test", id: "1", isDeleted: true) + } + + let received: [Syncable] = [ + .rootFolder(children: ["2"]), + .bookmark("test2", id: "2") + ] context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Bookmark("test2", id: "1") - Bookmark(id: "2") - }) + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() } + + let sent = try await provider.fetchChangedObjects(encryptedUsing: crypter) + + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["2"]) { + Bookmark("test2", id: "2") + }) } func testWhenObjectWasSentAndThenDeletedLocallyAndAnUpdateIsReceivedThenTheObjectIsDeleted() async throws { @@ -547,13 +576,8 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { try! context.save() } - try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: modifiedAt.addingTimeInterval(-1), serverTimestamp: "1234", crypter: crypter) - - context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - XCTAssertTrue(rootFolder.childrenArray.isEmpty) - } + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: modifiedAt.addingTimeInterval(-1), serverTimestamp: "1234", in: context) + XCTAssertTrue(rootFolder.childrenArray.isEmpty) } func testWhenObjectWasUpdatedLocallyAfterStartingSyncThenRemoteChangesAreDropped() async throws { @@ -585,15 +609,10 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { bookmarkModificationDate = bookmark.modifiedAt } - try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: Date().addingTimeInterval(-1), serverTimestamp: "1234", crypter: crypter) - - context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Bookmark("test3", id: "1", url: "test", modifiedAt: bookmarkModificationDate) - }) - } + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: Date().addingTimeInterval(-1), serverTimestamp: "1234", in: context) + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1"]) { + Bookmark("test3", id: "1", url: "test", modifiedAt: bookmarkModificationDate) + }) } func testWhenObjectWasUpdatedLocallyAfterStartingSyncThenRemoteDeletionIsApplied() async throws { @@ -622,13 +641,8 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { try! context.save() } - try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: Date().addingTimeInterval(-1), serverTimestamp: "1234", crypter: crypter) - - context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree {}) - } + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: Date().addingTimeInterval(-1), serverTimestamp: "1234", in: context) + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: []) {}) } func testWhenBookmarkIsMovedBetweenFoldersRemotelyAndUpdatedLocallyAfterStartingSyncThenItsModifiedAtIsNotCleared() async throws { @@ -668,21 +682,16 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { bookmarkModificationDate = bookmark.modifiedAt } - try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: bookmarkModificationDate.addingTimeInterval(-1), serverTimestamp: "1234", crypter: crypter) - - context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Folder(id: "1") - Folder(id: "2") { - // Bookmark retains non-nil modifiedAt, but it's newer than bookmarkModificationDate - // because it's updated after sync context save (bookmark object is not included in synced data - // but it gets updated as a side effect of sync – an update to parent). - Bookmark("test3", id: "3", url: "test", modifiedAtConstraint: .greaterThan(bookmarkModificationDate)) - } - }) - } + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: bookmarkModificationDate.addingTimeInterval(-1), serverTimestamp: "1234", in: context) + assertEquivalent(withLastChildrenArrayReceivedFromSync: false, rootFolder, BookmarkTree { + Folder(id: "1") + Folder(id: "2") { + // Bookmark retains non-nil modifiedAt, but it's newer than bookmarkModificationDate + // because it's updated after sync context save (bookmark object is not included in synced data + // but it gets updated as a side effect of sync – an update to parent). + Bookmark("test3", id: "3", url: "test", modifiedAtConstraint: .greaterThan(bookmarkModificationDate)) + } + }) } func testWhenThereIsMergeConflictDuringRegularSyncThenSyncResponseHandlingIsRetried() async throws { @@ -717,15 +726,10 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { bookmarkModificationDate = bookmark.modifiedAt } } - try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", crypter: crypter) - - context.performAndWait { - context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Bookmark("test3", id: "1", url: "test", modifiedAt: bookmarkModificationDate) - }) - } + let rootFolder = try await handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1"]) { + Bookmark("test3", id: "1", url: "test", modifiedAt: bookmarkModificationDate) + }) } // MARK: - syncDidFinish callback @@ -753,15 +757,96 @@ internal class BookmarksProviderTests: BookmarksProviderTestsBase { expectedSyncResult = .newData(modifiedIds: ["1", "3", "bookmarks_root"], deletedIds: ["2"]) - try await provider.handleSyncResponse(sent: [], received: received, clientTimestamp: Date(), serverTimestamp: "1234", crypter: crypter) + let rootFolder = try await handleSyncResponse(sent: [], received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "3"]) { + Bookmark("test1", id: "1") + Bookmark("test3", id: "3") + }) + } + + // MARK: - Last Children Array Received From Sync + func testThatLastChildrenArrayIsUpdatedAfterEveryHandledResponse() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark(id: "1") + Bookmark(id: "2") + } + + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() + } + + var received: [Syncable] = [ + .rootFolder(children: ["3"]), + .bookmark(id: "3") + ] + + var rootFolder = try await handleInitialSyncResponse(received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Bookmark(id: "3") + }) + + let sent = try await provider.fetchChangedObjects(encryptedUsing: crypter) + + rootFolder = try await handleSyncResponse(sent: sent, received: [], clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "3"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Bookmark(id: "3") + }) + + received = [ + .rootFolder(children: ["1", "2", "3", "4"]), + .bookmark(id: "4") + ] + + rootFolder = try await handleSyncResponse(sent: [], received: received, clientTimestamp: Date(), serverTimestamp: "1234", in: context) + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "3", "4"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Bookmark(id: "3") + Bookmark(id: "4") + }) + } + + // MARK: - Helpers + + func handleInitialSyncResponse( + received: [Syncable], + clientTimestamp: Date, + serverTimestamp: String?, + in context: NSManagedObjectContext + ) async throws -> BookmarkEntity { + + try await provider.handleInitialSyncResponse(received: received, clientTimestamp: clientTimestamp, serverTimestamp: serverTimestamp, crypter: crypter) + var rootFolder: BookmarkEntity! context.performAndWait { context.refreshAllObjects() - let rootFolder = BookmarkUtils.fetchRootFolder(context)! - assertEquivalent(rootFolder, BookmarkTree { - Bookmark("test1", id: "1") - Bookmark("test3", id: "3") - }) + rootFolder = BookmarkUtils.fetchRootFolder(context) + } + return rootFolder + } + + func handleSyncResponse( + sent: [Syncable], + received: [Syncable], + clientTimestamp: Date, + serverTimestamp: String?, + in context: NSManagedObjectContext + ) async throws -> BookmarkEntity { + + try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: clientTimestamp, serverTimestamp: serverTimestamp, crypter: crypter) + var rootFolder: BookmarkEntity! + context.performAndWait { + context.refreshAllObjects() + rootFolder = BookmarkUtils.fetchRootFolder(context) } + return rootFolder } } diff --git a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift index 21ea98364..752af2fb4 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift @@ -38,7 +38,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase let received: [Syncable] = [.rootFolder(children: ["2", "1"])] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["2", "1"]) { Bookmark(id: "2") Bookmark(id: "1") }) @@ -58,7 +58,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "3"]) { Bookmark(id: "1") Bookmark(id: "2") Bookmark(id: "3") @@ -79,7 +79,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3"]) { Bookmark(id: "1", isOrphaned: true) Bookmark(id: "2", isOrphaned: true) Bookmark(id: "3") @@ -103,7 +103,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder(id: "1") { + Folder(id: "1", lastChildrenArrayReceivedFromSync: ["4"]) { Bookmark(id: "4") } Bookmark(id: "2", isOrphaned: true) @@ -127,7 +127,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "3"]) { Bookmark(id: "1", favoritedOn: [.mobile, .unified]) Bookmark(id: "2", favoritedOn: [.mobile, .unified]) Bookmark(id: "3", favoritedOn: [.mobile, .unified]) @@ -154,7 +154,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { Bookmark(id: "1", favoritedOn: [.mobile, .unified]) - Folder(id: "2") { + Folder(id: "2", lastChildrenArrayReceivedFromSync: ["3"]) { Bookmark(id: "3", favoritedOn: [.mobile, .unified]) } }) @@ -200,7 +200,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "4"]) { Bookmark(id: "1", favoritedOn: [.mobile, .unified]) Folder(id: "2") { Bookmark(id: "3", favoritedOn: [.mobile, .unified]) @@ -224,7 +224,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3", "2"]) { Bookmark(id: "1", isOrphaned: true) Bookmark(id: "3") Bookmark(id: "2") @@ -245,7 +245,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["2"]) { Bookmark(id: "2") }) } @@ -268,7 +268,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2"]) { Bookmark(id: "1") Bookmark(id: "2") }) @@ -291,7 +291,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3", "remote2", "4"]) { Bookmark(id: "3") Bookmark("2", id: "remote2") Bookmark(id: "4") @@ -321,7 +321,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { Bookmark(id: "1") Bookmark(id: "2") - Folder(id: "3") { + Folder(id: "3", lastChildrenArrayReceivedFromSync: ["5", "4"]) { Bookmark("title", id: "5", url: "url") Bookmark("title", id: "4", url: "url") } @@ -355,7 +355,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase Bookmark(id: "2") Folder(id: "3") { Folder(id: "4") - Folder(id: "6") { + Folder(id: "6", lastChildrenArrayReceivedFromSync: ["5"]) { Bookmark("title", id: "5", url: "url") } } @@ -403,7 +403,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { Bookmark(id: "1") Bookmark(id: "2") - Folder(id: "3") { + Folder(id: "3", lastChildrenArrayReceivedFromSync: ["6", "4"]) { Folder(id: "6") { Bookmark("title7", id: "7", url: "url7") } @@ -411,13 +411,13 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase Bookmark("title5", id: "5", url: "url5") } } - Folder(id: "8") { + Folder(id: "8", lastChildrenArrayReceivedFromSync: ["10", "9"]) { Bookmark("title10", id: "10", url: "url10") Bookmark("title9", id: "9", url: "url9") } - Folder(id: "11") { + Folder(id: "11", lastChildrenArrayReceivedFromSync: ["12", "14", "13"]) { Bookmark("title12", id: "12", url: "url12") - Folder(id: "14") { + Folder(id: "14", lastChildrenArrayReceivedFromSync: ["18", "15", "16"]) { Bookmark("title16", id: "18", url: "url16") Bookmark("title15", id: "15", url: "url15") Bookmark("title16", id: "16", url: "url16") @@ -445,7 +445,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder("Folder", id: "1") { + Folder("Folder", id: "1", lastChildrenArrayReceivedFromSync: ["2", "3", "5", "6"]) { Bookmark(id: "2") Bookmark(id: "3") Bookmark(id: "5") @@ -469,12 +469,12 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase ] let rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) - assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { - Folder(id: "1") - Folder(id: "2") { + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2"]) { + Folder(id: "1", lastChildrenArrayReceivedFromSync: []) + Folder(id: "2", lastChildrenArrayReceivedFromSync: ["3"]) { Bookmark("name", id: "3", url: "url") } - Folder(id: "4", isOrphaned: true) { + Folder(id: "4", isOrphaned: true, lastChildrenArrayReceivedFromSync: ["5"]) { Bookmark(id: "5") } }) @@ -517,7 +517,7 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree { Bookmark(id: "1") Bookmark(id: "2") - Folder(id: "3", isOrphaned: true) { + Folder(id: "3", isOrphaned: true, lastChildrenArrayReceivedFromSync: ["4"]) { Bookmark(id: "4") } }) @@ -583,6 +583,64 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase }) } + // MARK: - Last Children Array Received From Sync + + func testThatLastChildrenArrayIsUpdatedAfterEveryHandledResponse() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark(id: "1") + Bookmark(id: "2") + } + + var received: [Syncable] = [ + .rootFolder(children: ["1", "2", "3"]), + .bookmark(id: "3") + ] + + var rootFolder = try await createEntitiesAndHandleSyncResponse(with: bookmarkTree, received: received, in: context) + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "3"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Bookmark(id: "3") + }) + + received = [ + .rootFolder(children: ["1", "2", "4", "3"]), + .folder(id: "4", children: ["5", "6"]), + .bookmark(id: "3"), + .bookmark(id: "5"), + .bookmark(id: "6") + ] + + rootFolder = try await handleSyncResponse(received: received, in: context) + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "4", "3"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Folder(id: "4", lastChildrenArrayReceivedFromSync: ["5", "6"]) { + Bookmark(id: "5") + Bookmark(id: "6") + } + Bookmark(id: "3") + }) + + received = [ + .rootFolder(children: ["3", "4"]), + .folder(id: "4", children: ["6"]), + .bookmark(id: "1", isDeleted: true), + .bookmark(id: "2", isDeleted: true), + .bookmark(id: "5", isDeleted: true) + ] + + rootFolder = try await handleSyncResponse(received: received, in: context) + assertEquivalent(withTimestamps: false, rootFolder, BookmarkTree(lastChildrenArrayReceivedFromSync: ["3", "4"]) { + Bookmark(id: "3") + Folder(id: "4", lastChildrenArrayReceivedFromSync: ["6"]) { + Bookmark(id: "6") + } + }) + } + // MARK: - Helpers func createEntitiesAndHandleSyncResponse( @@ -600,6 +658,23 @@ final class BookmarksRegularSyncResponseHandlerTests: BookmarksProviderTestsBase try! context.save() } + return try await handleSyncResponse( + sent: sent, + received: received, + clientTimestamp: clientTimestamp, + serverTimestamp: serverTimestamp, + in: context + ) + } + + func handleSyncResponse( + sent: [Syncable] = [], + received: [Syncable], + clientTimestamp: Date = Date(), + serverTimestamp: String = "1234", + in context: NSManagedObjectContext + ) async throws -> BookmarkEntity { + try await provider.handleSyncResponse(sent: sent, received: received, clientTimestamp: Date(), serverTimestamp: "1234", crypter: crypter) var rootFolder: BookmarkEntity! diff --git a/Tests/SyncDataProvidersTests/Bookmarks/SyncableBookmarkAdapterTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/SyncableBookmarkAdapterTests.swift new file mode 100644 index 000000000..741b808ab --- /dev/null +++ b/Tests/SyncDataProvidersTests/Bookmarks/SyncableBookmarkAdapterTests.swift @@ -0,0 +1,325 @@ +// +// SyncableBookmarkAdapterTests.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Bookmarks +import BookmarksTestsUtils +import Common +import DDGSync +import Persistence +@testable import SyncDataProviders + +final class SyncableBookmarkAdapterTests: BookmarksProviderTestsBase { + + func testThatLastChildrenArrayReceivedFromSyncIsSerializedAndDeserializedCorrectly() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + let bookmarkEntity = BookmarkEntity(context: context) + + bookmarkEntity.lastChildrenArrayReceivedFromSync = nil + XCTAssertEqual(bookmarkEntity.lastChildrenArrayReceivedFromSync, nil) + + bookmarkEntity.lastChildrenArrayReceivedFromSync = [] + XCTAssertEqual(bookmarkEntity.lastChildrenArrayReceivedFromSync, []) + + bookmarkEntity.lastChildrenArrayReceivedFromSync = ["1"] + XCTAssertEqual(bookmarkEntity.lastChildrenArrayReceivedFromSync, ["1"]) + + bookmarkEntity.lastChildrenArrayReceivedFromSync = [""] + XCTAssertEqual(bookmarkEntity.lastChildrenArrayReceivedFromSync, []) + + bookmarkEntity.lastChildrenArrayReceivedFromSync = ["1", "2", "3"] + XCTAssertEqual(bookmarkEntity.lastChildrenArrayReceivedFromSync, ["1", "2", "3"]) + } + + func testThatLastChildrenArrayReceivedFromSyncIgnoresEmptyIdentifiers() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + let bookmarkEntity = BookmarkEntity(context: context) + + bookmarkEntity.lastChildrenArrayReceivedFromSync = [""] + XCTAssertEqual(bookmarkEntity.lastChildrenArrayReceivedFromSync, []) + + bookmarkEntity.lastChildrenArrayReceivedFromSync = ["", "", ""] + XCTAssertEqual(bookmarkEntity.lastChildrenArrayReceivedFromSync, []) + + bookmarkEntity.lastChildrenArrayReceivedFromSync = ["", "1", "", "", "2", ""] + XCTAssertEqual(bookmarkEntity.lastChildrenArrayReceivedFromSync, ["1", "2"]) + } + + func testThatAddingBookmarksToRootFolderReportsAllBookmarksAsInserted() async throws { + let bookmarkTree = BookmarkTree(modifiedAt: Date(), lastChildrenArrayReceivedFromSync: []) { + Bookmark(id: "1") + Bookmark(id: "2") + Bookmark(id: "16") + Bookmark(id: "3") + Bookmark(id: "4") + } + populateBookmarks(with: bookmarkTree) + + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == BookmarkEntity.Constants.rootFolderID }) + XCTAssertEqual(rootFolderSyncable.children, ["1", "2", "16", "3", "4"]) + XCTAssertEqual(rootFolderSyncable.inserted, ["1", "2", "16", "3", "4"]) + XCTAssertEqual(rootFolderSyncable.removed, nil) + } + + func testThatAddingBookmarksToSubfolderReportsAllBookmarksAsInserted() async throws { + let bookmarkTree = BookmarkTree(lastChildrenArrayReceivedFromSync: ["1", "2", "5", "16", "3", "4"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Folder(id: "5", lastChildrenArrayReceivedFromSync: []) { + Bookmark(id: "7") + Bookmark(id: "6") + Bookmark(id: "8") + } + Bookmark(id: "16") + Bookmark(id: "3") + Bookmark(id: "4") + } + populateBookmarks(with: bookmarkTree) + + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == "5" }) + XCTAssertEqual(rootFolderSyncable.children, ["7", "6", "8"]) + XCTAssertEqual(rootFolderSyncable.inserted, ["7", "6", "8"]) + XCTAssertEqual(rootFolderSyncable.removed, nil) + } + + func testThatDeletingAllBookmarksReportsAllBookmarksAsRemoved() async throws { + let timestamp = Date() + let bookmarkTree = BookmarkTree(modifiedAt: timestamp, lastChildrenArrayReceivedFromSync: ["1", "4", "3", "16", "2"]) { + Bookmark(id: "1", isDeleted: true) + Bookmark(id: "2", isDeleted: true) + Bookmark(id: "16", isDeleted: true) + Bookmark(id: "3", isDeleted: true) + Bookmark(id: "4", isDeleted: true) + } + populateBookmarks(with: bookmarkTree) + + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == BookmarkEntity.Constants.rootFolderID }) + XCTAssertEqual(rootFolderSyncable.children, []) + XCTAssertEqual(rootFolderSyncable.inserted, nil) + XCTAssertEqual(rootFolderSyncable.removed, ["1", "2", "16", "3", "4"]) + } + + func testThatDeletingAllBookmarksFromSubfolderReportsAllBookmarksAsRemoved() async throws { + let timestamp = Date() + let bookmarkTree = BookmarkTree(modifiedAt: timestamp, lastChildrenArrayReceivedFromSync: ["1", "2", "5", "16", "3", "4"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Folder(id: "5", lastChildrenArrayReceivedFromSync: ["7", "6", "8"]) { + Bookmark(id: "7", isDeleted: true) + Bookmark(id: "6", isDeleted: true) + Bookmark(id: "8", isDeleted: true) + } + Bookmark(id: "16") + Bookmark(id: "3") + Bookmark(id: "4") + } + populateBookmarks(with: bookmarkTree) + + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == "5" }) + XCTAssertEqual(rootFolderSyncable.children, []) + XCTAssertEqual(rootFolderSyncable.inserted, nil) + XCTAssertEqual(rootFolderSyncable.removed, ["7", "6", "8"]) + } + + func testThatUponInitialSyncFolderOnlySubmitsChildrenWithoutInsertedOrRemoved() async throws { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + + let bookmarkTree = BookmarkTree { + Bookmark(id: "1") + Bookmark(id: "4") + Bookmark(id: "3") + Bookmark(id: "16") + Bookmark(id: "2") + } + + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() + } + + try provider.prepareForFirstSync() + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == BookmarkEntity.Constants.rootFolderID }) + XCTAssertEqual(rootFolderSyncable.children, ["1", "4", "3", "16", "2"]) + XCTAssertEqual(rootFolderSyncable.inserted, ["1", "4", "3", "16", "2"]) + XCTAssertEqual(rootFolderSyncable.removed, nil) + } + + func testThatUponInitialSyncSubfolderOnlySubmitsChildrenWithoutInsertedOrRemoved() async throws { + let bookmarkTree = BookmarkTree { + Bookmark(id: "1") + Bookmark(id: "4") + Folder(id: "5", lastChildrenArrayReceivedFromSync: ["7", "6", "8"]) { + Bookmark(id: "7") + Bookmark(id: "6") + Bookmark(id: "8") + } + Bookmark(id: "3") + Bookmark(id: "16") + Bookmark(id: "2") + } + populateBookmarks(with: bookmarkTree) + + try provider.prepareForFirstSync() + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == "5" }) + XCTAssertEqual(rootFolderSyncable.children, ["7", "6", "8"]) + XCTAssertEqual(rootFolderSyncable.inserted, ["7", "6", "8"]) + XCTAssertEqual(rootFolderSyncable.removed, nil) + } + + func testThatNewChildrenOfExistingFolderAreReportedAsInserted() async throws { + let timestamp = Date() + let bookmarkTree = BookmarkTree(modifiedAt: timestamp, lastChildrenArrayReceivedFromSync: ["1", "4", "3"]) { + Bookmark(id: "1") + Bookmark(id: "4") + Bookmark(id: "3") + Bookmark(id: "16", modifiedAt: timestamp) + Bookmark(id: "2", modifiedAt: timestamp) + } + populateBookmarks(with: bookmarkTree) + + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == BookmarkEntity.Constants.rootFolderID }) + XCTAssertEqual(rootFolderSyncable.children, ["1", "4", "3", "16", "2"]) + XCTAssertEqual(rootFolderSyncable.inserted, ["16", "2"]) + XCTAssertEqual(rootFolderSyncable.removed, nil) + } + + func testThatDeletedChildrenOfExistingFolderAreReportedAsRemoved() async throws { + let timestamp = Date() + let bookmarkTree = BookmarkTree(modifiedAt: timestamp, lastChildrenArrayReceivedFromSync: ["1", "4", "3", "16", "2"]) { + Bookmark(id: "1") + Bookmark(id: "4") + Bookmark(id: "3") + Bookmark(id: "16", modifiedAt: timestamp, isDeleted: true) + Bookmark(id: "2", modifiedAt: timestamp, isDeleted: true) + } + populateBookmarks(with: bookmarkTree) + + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == BookmarkEntity.Constants.rootFolderID }) + XCTAssertEqual(rootFolderSyncable.children, ["1", "4", "3"]) + XCTAssertEqual(rootFolderSyncable.inserted, nil) + XCTAssertEqual(rootFolderSyncable.removed, ["16", "2"]) + } + + func testThatReorderedChildrenOfExistingFolderAreNotReportedInInsertedOrRemoved() async throws { + let timestamp = Date() + let bookmarkTree = BookmarkTree(modifiedAt: timestamp, lastChildrenArrayReceivedFromSync: ["1", "4", "3", "16", "2"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Bookmark(id: "16") + Bookmark(id: "3") + Bookmark(id: "4") + } + populateBookmarks(with: bookmarkTree) + + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == BookmarkEntity.Constants.rootFolderID }) + XCTAssertEqual(rootFolderSyncable.children, ["1", "2", "16", "3", "4"]) + XCTAssertEqual(rootFolderSyncable.inserted, nil) + XCTAssertEqual(rootFolderSyncable.removed, nil) + } + + func testThatInsertedAndRemovedChildrenOfExistingFolderAreReportedInInsertedAndRemoved() async throws { + let timestamp = Date() + let bookmarkTree = BookmarkTree(modifiedAt: timestamp, lastChildrenArrayReceivedFromSync: ["1", "4", "3", "16", "2"]) { + Bookmark(id: "1") + Bookmark(id: "2") + Folder(id: "5", lastChildrenArrayReceivedFromSync: ["7", "6", "10", "8"]) { + Bookmark(id: "7", isDeleted: true) + Bookmark(id: "8") + Bookmark(id: "9") + Bookmark(id: "6") + Bookmark(id: "10", isDeleted: true) + } + Bookmark(id: "16") + Bookmark(id: "3") + Bookmark(id: "4") + } + populateBookmarks(with: bookmarkTree) + + let changedObjects = try await provider.fetchChangedObjects(encryptedUsing: crypter) + .map(SyncableBookmarkAdapter.init(syncable:)) + + let rootFolderSyncable = try XCTUnwrap(changedObjects.first { $0.uuid == "5" }) + XCTAssertEqual(rootFolderSyncable.children, ["8", "9", "6"]) + XCTAssertEqual(rootFolderSyncable.inserted, ["9"]) + XCTAssertEqual(rootFolderSyncable.removed, ["7", "10"]) + } + + // MARK: - Private + + private func populateBookmarks(with bookmarkTree: BookmarkTree) { + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) + context.performAndWait { + BookmarkUtils.prepareFoldersStructure(in: context) + bookmarkTree.createEntities(in: context) + try! context.save() + } + } +} + +private extension SyncableBookmarkAdapter { + var inserted: Set? { + guard let folder = syncable.payload["folder"] as? [String: Any], + let folderChildrenDictionary = folder["children"] as? [String: Any], + let insertedChildren = folderChildrenDictionary["insert"] as? [String] + else { + return nil + } + + return Set(insertedChildren) + } + + var removed: Set? { + guard let folder = syncable.payload["folder"] as? [String: Any], + let folderChildrenDictionary = folder["children"] as? [String: Any], + let removedChildren = folderChildrenDictionary["remove"] as? [String] + else { + return nil + } + + return Set(removedChildren) + } +} From a72cd4cd3ba49d5468d3866a8ec7138ee6100075 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Tue, 5 Dec 2023 17:26:17 +0100 Subject: [PATCH 13/39] Re-enable Bookmarks migration tests (#585) Required: Task/Issue URL: https://app.asana.com/0/0/1206094946248830/f iOS PR: n/a macOS PR: n/a What kind of version bump will this require?: n/a --- ...BookmarkFormFactorFavoritesMigration.swift | 16 ++++++- .../ModelAccessHelper.swift | 44 +++++++++++++++++++ .../BookmarkMigrationTests.swift | 5 +-- 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 Sources/BookmarksTestsUtils/ModelAccessHelper.swift diff --git a/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift b/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift index 4f73e8b38..5a7745764 100644 --- a/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift +++ b/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift @@ -38,7 +38,21 @@ public class BookmarkFormFactorFavoritesMigration { // Before migrating to latest scheme version, read order of favorites from DB - let oldBookmarksModel = NSManagedObjectModel.mergedModel(from: [Bookmarks.bundle], forStoreMetadata: metadata)! + let oldBookmarksModel: NSManagedObjectModel = { + var mergedModel = NSManagedObjectModel.mergedModel(from: [Bookmarks.bundle], forStoreMetadata: metadata) +#if DEBUG && os(macOS) + if mergedModel == nil { + /// Look for individual model files in the bundle because if they have just + /// been added there by `ModelAccessHelper.compileModel(from:named:)` in the same run, + /// they wouldn't be visible to `NSManagedObjectModel.mergedModel(from:forStoreMetadata:)`. + let modelURLs = Bookmarks.bundle.urls(forResourcesWithExtension: "mom", subdirectory: "BookmarksModel.momd") ?? [] + let models = modelURLs.compactMap(NSManagedObjectModel.init(contentsOf:)) + mergedModel = NSManagedObjectModel(byMerging: models, forStoreMetadata: metadata) + } +#endif + return mergedModel! + }() + let oldDB = CoreDataDatabase(name: dbFileURL.deletingPathExtension().lastPathComponent, containerLocation: dbContainerLocation, model: oldBookmarksModel) diff --git a/Sources/BookmarksTestsUtils/ModelAccessHelper.swift b/Sources/BookmarksTestsUtils/ModelAccessHelper.swift new file mode 100644 index 000000000..8b6a8a01c --- /dev/null +++ b/Sources/BookmarksTestsUtils/ModelAccessHelper.swift @@ -0,0 +1,44 @@ +// +// ModelAccessHelper.swift +// DuckDuckGo +// +// 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 + +public class ModelAccessHelper { + + public static func compileModel(from bundle: Bundle, named name: String) { + let momdUrl = bundle.url(forResource: name, withExtension: "momd") ?? + bundle.resourceURL!.appendingPathComponent(name + ".momd") +#if DEBUG && os(macOS) + // when running tests using `swift test` xcdatamodeld is not compiled to momd for some reason + // this is a workaround to compile it in runtime + if !FileManager.default.fileExists(atPath: momdUrl.path), + let xcDataModelUrl = bundle.url(forResource: name, withExtension: "xcdatamodeld"), + let sdkRoot = ProcessInfo().environment["SDKROOT"], + let developerDir = sdkRoot.range(of: "/Contents/Developer").map({ sdkRoot[..<$0.upperBound] }) { + + let compileDataModel = Process() + let momc = "\(developerDir)/usr/bin/momc" + compileDataModel.executableURL = URL(fileURLWithPath: momc) + compileDataModel.arguments = [xcDataModelUrl.path, momdUrl.path] + try? compileDataModel.run() + compileDataModel.waitUntilExit() + } +#endif + } + +} diff --git a/Tests/BookmarksTests/BookmarkMigrationTests.swift b/Tests/BookmarksTests/BookmarkMigrationTests.swift index a610105b0..3f13e1d94 100644 --- a/Tests/BookmarksTests/BookmarkMigrationTests.swift +++ b/Tests/BookmarksTests/BookmarkMigrationTests.swift @@ -31,6 +31,8 @@ class BookmarkMigrationTests: XCTestCase { override func setUp() { super.setUp() + ModelAccessHelper.compileModel(from: Bundle(for: BookmarkMigrationTests.self), named: "BookmarksModel") + location = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) guard let location = Bundle(for: BookmarkMigrationTests.self).resourceURL else { @@ -75,17 +77,14 @@ class BookmarkMigrationTests: XCTestCase { } func testWhenMigratingFromV1ThenRootFoldersContentsArePreservedInOrder() throws { - throw XCTSkip("Won't run on CI or from command line as momd is not compiled. Tested through Xcode") try commonMigrationTestForDatabase(name: "Bookmarks_V1") } func testWhenMigratingFromV2ThenRootFoldersContentsArePreservedInOrder() throws { - throw XCTSkip("Won't run on CI or from command line as momd is not compiled. Tested through Xcode") try commonMigrationTestForDatabase(name: "Bookmarks_V2") } func testWhenMigratingFromV3ThenRootFoldersContentsArePreservedInOrder() throws { - throw XCTSkip("Won't run on CI or from command line as momd is not compiled. Tested through Xcode") try commonMigrationTestForDatabase(name: "Bookmarks_V3") } From bba78df5b33387502973387fd2a4f4ed0a80fce5 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 5 Dec 2023 11:32:10 -0800 Subject: [PATCH 14/39] Add an option to disable rekeying. (#587) Task/Issue URL: https://app.asana.com/0/1199333091098016/1206100276423377/f iOS PR: duckduckgo/iOS#2219 macOS PR: duckduckgo/macos-browser#1922 What kind of version bump will this require?: Major Optional: Tech Design URL: CC: Description: This PR adds a setting to disable rekeying. --- .../PacketTunnelProvider.swift | 12 +++-- .../UserDefaults+disableRekeying.swift | 47 +++++++++++++++++++ .../Settings/VPNSettings.swift | 26 +++++++++- 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 Sources/NetworkProtection/Settings/Extensions/UserDefaults+disableRekeying.swift diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 79ccc6f03..d3ed84220 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -156,11 +156,16 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } private func rekey() async { + providerEvents.fire(.userBecameActive) + + // Experimental option to disable rekeying. + guard !settings.disableRekeying else { + return + } + os_log("Rekeying...", log: .networkProtectionKeyManagement) - providerEvents.fire(.userBecameActive) providerEvents.fire(.rekeyCompleted) - self.resetRegistrationKey() do { @@ -831,7 +836,8 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { .setRegistrationKeyValidity, .setSelectedEnvironment, .setShowInMenuBar, - .setVPNFirstEnabled: + .setVPNFirstEnabled, + .setDisableRekeying: // Intentional no-op, as some setting changes don't require any further operation break } diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+disableRekeying.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+disableRekeying.swift new file mode 100644 index 000000000..c3d8bda32 --- /dev/null +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+disableRekeying.swift @@ -0,0 +1,47 @@ +// +// UserDefaults+disableRekeying.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 Combine +import Foundation + +extension UserDefaults { + private var disableRekeyingKey: String { + "networkProtectionSettingDisableRekeying" + } + + static let disableRekeyingDefaultValue = false + + @objc + dynamic var networkProtectionSettingDisableRekeying: Bool { + get { + value(forKey: disableRekeyingKey) as? Bool ?? Self.disableRekeyingDefaultValue + } + + set { + set(newValue, forKey: disableRekeyingKey) + } + } + + var networkProtectionSettingDisableRekeyingPublisher: AnyPublisher { + publisher(for: \.networkProtectionSettingDisableRekeying).eraseToAnyPublisher() + } + + func resetNetworkProtectionSettingDisableRekeying() { + removeObject(forKey: disableRekeyingKey) + } +} diff --git a/Sources/NetworkProtection/Settings/VPNSettings.swift b/Sources/NetworkProtection/Settings/VPNSettings.swift index a8b21ea94..e9c7fb3db 100644 --- a/Sources/NetworkProtection/Settings/VPNSettings.swift +++ b/Sources/NetworkProtection/Settings/VPNSettings.swift @@ -41,6 +41,7 @@ public final class VPNSettings { case setSelectedEnvironment(_ selectedEnvironment: SelectedEnvironment) case setShowInMenuBar(_ showInMenuBar: Bool) case setVPNFirstEnabled(_ vpnFirstEnabled: Date?) + case setDisableRekeying(_ disableRekeying: Bool) } public enum RegistrationKeyValidity: Codable { @@ -136,6 +137,10 @@ public final class VPNSettings { Change.setVPNFirstEnabled(vpnFirstEnabled) }.eraseToAnyPublisher() + let disableRekeyingPublisher = disableRekeyingPublisher.map { disableRekeying in + Change.setDisableRekeying(disableRekeying) + }.eraseToAnyPublisher() + return Publishers.MergeMany( connectOnLoginPublisher, includeAllNetworksPublisher, @@ -146,7 +151,8 @@ public final class VPNSettings { locationChangePublisher, environmentChangePublisher, showInMenuBarPublisher, - vpnFirstEnabledPublisher).eraseToAnyPublisher() + vpnFirstEnabledPublisher, + disableRekeyingPublisher).eraseToAnyPublisher() }() public init(defaults: UserDefaults) { @@ -194,6 +200,8 @@ public final class VPNSettings { self.showInMenuBar = showInMenuBar case .setVPNFirstEnabled(let vpnFirstEnabled): self.vpnFirstEnabled = vpnFirstEnabled + case .setDisableRekeying(let disableRekeying): + self.disableRekeying = disableRekeying } } // swiftlint:enable cyclomatic_complexity @@ -401,6 +409,22 @@ public final class VPNSettings { defaults.vpnFirstEnabled = newValue } } + + // MARK: - Disable Rekeying + + public var disableRekeyingPublisher: AnyPublisher { + defaults.networkProtectionSettingDisableRekeyingPublisher + } + + public var disableRekeying: Bool { + get { + defaults.networkProtectionSettingDisableRekeying + } + + set { + defaults.networkProtectionSettingDisableRekeying = newValue + } + } } // swiftlint:enable type_body_length file_length From aa5cd7681739a17ca3c4d6b08387387334dbaeb2 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 6 Dec 2023 09:37:54 +0100 Subject: [PATCH 15/39] Add migration tests for Bookmarks model v5 (#588) Task/Issue URL: https://app.asana.com/0/414709148257752/1206106768488096/f Description: Update BookmarkMigrationTests with a test for migration from V4 to V5. --- Package.swift | 5 ++++- .../BookmarksTests/BookmarkMigrationTests.swift | 6 +++++- .../Resources/Bookmarks_V4.sqlite | Bin 0 -> 57344 bytes .../Resources/Bookmarks_V4.sqlite-shm | Bin 0 -> 32768 bytes .../Resources/Bookmarks_V4.sqlite-wal | Bin 0 -> 41232 bytes 5 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V4.sqlite create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V4.sqlite-shm create mode 100644 Tests/BookmarksTests/Resources/Bookmarks_V4.sqlite-wal diff --git a/Package.swift b/Package.swift index 89eddfd63..fa8c4e038 100644 --- a/Package.swift +++ b/Package.swift @@ -251,7 +251,10 @@ let package = Package( .copy("Resources/Bookmarks_V2.sqlite-wal"), .copy("Resources/Bookmarks_V3.sqlite"), .copy("Resources/Bookmarks_V3.sqlite-shm"), - .copy("Resources/Bookmarks_V3.sqlite-wal") + .copy("Resources/Bookmarks_V3.sqlite-wal"), + .copy("Resources/Bookmarks_V4.sqlite"), + .copy("Resources/Bookmarks_V4.sqlite-shm"), + .copy("Resources/Bookmarks_V4.sqlite-wal") ]), .testTarget( name: "BrowserServicesKitTests", diff --git a/Tests/BookmarksTests/BookmarkMigrationTests.swift b/Tests/BookmarksTests/BookmarkMigrationTests.swift index 3f13e1d94..baae0fb03 100644 --- a/Tests/BookmarksTests/BookmarkMigrationTests.swift +++ b/Tests/BookmarksTests/BookmarkMigrationTests.swift @@ -88,6 +88,10 @@ class BookmarkMigrationTests: XCTestCase { try commonMigrationTestForDatabase(name: "Bookmarks_V3") } + func testWhenMigratingFromV4ThenRootFoldersContentsArePreservedInOrder() throws { + try commonMigrationTestForDatabase(name: "Bookmarks_V4") + } + func commonMigrationTestForDatabase(name: String) throws { try copyDatabase(name: name, formDirectory: resourceURLDir, toDirectory: location) @@ -131,7 +135,7 @@ class BookmarkMigrationTests: XCTestCase { try? migratedStack.tearDown(deleteStores: true) } - func atestThatMigrationToFormFactorSpecificFavoritesAddsFavoritesToNativeFolder() async throws { + func testThatMigrationToFormFactorSpecificFavoritesAddsFavoritesToNativeFolder() async throws { guard let bookmarksDatabase = loadDatabase(name: "Any") else { XCTFail("Failed to load model") diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V4.sqlite b/Tests/BookmarksTests/Resources/Bookmarks_V4.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..248921609f6f913e179bc90c25d5158ca29a582d GIT binary patch literal 57344 zcmeI%2{@E%-vIC#gF*JaBxB8*Jxj>cju0_Qk#djHGsbaB=X3bBYPFC=^m zA;@O>!Ag_$7Dw4*c z1%%TnRA*OPJQ0V*IpAmR3*$WSSTY_%#@Z0^n3-oV>J%!@)rE|AA!BTat~MI0=b&X2 z6_KgfF9GpntSy#|WmlTs6Z$&X^l?roFT4kd;Oc@Qup`>zJuq$_1ZS*=FUAS)tIfWz zrzgP{my3euL`+{F(@aGv$ z4xFa^&HY`l&iF6)=iHj)>BdfS`eM!jXRObcqDHDVKU&693zc30Z(}Va` zY=Q43$gcA3lsG5;ngpBhKg(4cjh0!$&UNkI=E@!^UnR@`-DEkV>zja|#@+uSU3Ffx zj5|AB+PBkXd}kc+{rk-SQvKVF_|Ifzc+fIV>?g=+I#qo;te2|?fs7|nDbyK-Ph)cC z5%gC{&bW5wWz^4unZrCI9Ot?FvjXQy`lDp$O0;Fq3L2>hZnTU&8&U3aX0sm^D#Z=! z!G3B!hySYQd1t5g?`nRV?sII7R7FMQZ?x>o zgfXktq?t$UT%G>(m~m#t>^KW>nrwDJ4UC2fikH*3h);mA(c$dHDwP(;Vty*m?q2e$BVEzAJVTGU|00JNY0w4eaAOHd&00JNY z0wC~H2?%hqf&c&iRDA-;fB*=900@8p2!H?xfB*=900@Ag5ku>Sw~`UElp0T2KI5C8!X009sH0T2KI z5CDN6Eg-}Wh!lB>V1Hl(0T2KI5C8!X009sH0T2KI5C8!X_`fe8f#gAG@bTF&nPCyM zm@s@Ki@{3#wEh=rM6f@wfdB}A00@8p2!H?xfB*=900{iN0_j%lMV;rS#rVahrg$9I zOvly?XQE@IhcnhOGqN$#!Rp(Y;@D@gMh2zL_~ZY4c1EH*B}iJnLmo%^>TsG{;NBsWf4u`GII zAl)v87C~Rdj0u}gl`4w)+q%=2lnA(ak=%(47Tw)|D(Vw#n6%O(Fr1+u@2T%Z)Ti5f zhiTb_MR^<3DMVs;Sct7Njo=Z?Fp0n!(#+`g5f1T=@pz^~urrqCgY_li%sgp^k+fK& z5PdgOn$Ie|1ctk_L5QU#RaBd?l1?-*GVl&FaJx5nqnCHxQ6kc&R+t8*6Kmg2 zit!kWq7ZzDxd?NF9by?`4PqbS6rvCD0r3%uK%$ZS$RK16G7njfY)5u}9=cq}=^-pR zdnh9>e_H?Zh#}Y?*gyaTK;ZwkKyEk^b;y64H-8k_Q@Wx0Cid;8MxudBd3rJ#Sx7>3L!TJ;50xJtO|gh1@~?3+fEdd)D|cudQi$K{#N| zb<#jwVD7G|o9`stuxVsjWZn7EDD+ShSv`G830EjhRaP`j+DGJY7q8slf?mF*8P4PC ztR`dvzyAhY6xW@NhEZZkgUs<&^;}djn{ZoYmuy?!obDj)0^$Tgv49Yql^7)EYoqC) z>T5T$DgSK;MO?-Gs?C_wJU#eDUi$3N;3$EvmvQbjO8kA|T^1LK6 zFQvKK{`PMy?-s7x<3}`ldRkr`bgwkoh+2^5>#q^_!T6j^dLB|oluw;rMyCtXfe8Scgk240V75DU2x|x_r zhK5es7GA3Ab7|(@&6l%cB1Y?G@d4lJ+Q`={w;kGVD*obl++g}%ig$~LLmEZdMOV^2LqKK)@1=BKJvsYF*7}z+65Y67(vaHecnof_ zcI(6~4pYH>wO>Lp>a~Xw9C( zzf5$qPxz^#$_8_vq0A%aPgaNz{I<0|#xc#Rq1JXslJf6IQ;)fsXKs=V72X(t{RiVl zsn&zSu)x7X%cZ1hk}Qv4Un*>vRq7Q=P zLJLo<@%0ta3-w!`-NG3kD(hTvvcF*KlU703HP*T>R{N&!b6B>p(92qQLtNl9^P8GJ zDB~+Q`FfghLh*t#gDClV#aY7p`L7cs7~;+q8b-Ml-LD><7YQrj=jsx~@?m9fV$*}W zh~~S650pNZO_GzhYwLbNJ|w)_C$HCOo`p`yrn}aKD663p(#ssrZ9Fl2S-RnDK&ikj zzQ?&WyYG5G-9eGa_oKF%PTF_ADi-n8>_(Y8l_*K`FHgU0O`9{YC2>mdMAMq1LMKY? z;?I2;M@Zf?o*y4@{7{Mf^O5mBo?F9X205zQ`|pV)$>w=h^*!|rw9wa0O4KgildUfF z>j59#9@N9B$Me?8<~|?mITyN}ue`gw!rCHrn7HonOE(2OziLNkhTfK?D=BWYv6O&- zV|%^FGudGn6^P@Zu9Nk4Lr;T_m#rU7EFzC?I2j!&}^q(>*K0}_h6Y+{WsnI%jnB| zssoK9$1iYgFuOKNY))w&(^THK;<%zDlgbVEUfMUynq!6}0*IYL`ubXk|=)+VX2BR$G8(cRL98v>)GkRpNFZwfB$>E`SzfM(fdRF#3QEDGof@bs*)>8HW~Em%tPb972|B5&7*Lnl%eRgD zQvJG`7VE2wq%$Z9^jur^4TuvAI(qFr95e~aPgqKqOEXx!Mc14+ z>?9gUqM`!o2|?VFS?=0F(nrfJEJk)EMwz_gzI5IodEkRTd0QnRc4dk+vz1rAC4HSs z(|C+deqfZ0nlH;h>XcZ@lDYQ3b|l0s`PCq`%=-N{5}7KgHnvQA4R=O~lzoF2>VR3h zLRaRm1&cZq6E?}-5efLmu)NBS%D3oX-vpAw`E${%ot1(sF|i7i4x_R-^U~ah%9bQq z%NwsFJUz=*rJat^lFj$|5{Mt!nOJM&b-#}H5RA30t=o*&r1qw zD<05%<&%;>d~~x}m6|cjyy2zS#%+9s!RsroHKok2Xpxt`Gt{+FSdO^Su=}3&L(x^O z|2%PZ*&B-1I<<-|h@#v7SakJ)aPWmgp*wy2o?|aqq{WM;71`(5E+Qf4*ho2;(rD<^g9I|r7YWWMFL+NRL_X#C~7jAV_*-P9)C;pY#v-fEuRKy5|8 z(^I%7bT)%IY%(tQ&e>x4`EcEY$b{?5D-*gC)i2QR>fYu|9hjny;zpI9@l5(ow!~K# z^{whts9PGcwj-q}{i0D@^zBC znmot`Wzv@24X;&4WY+@137d7ZKD6i4*%R1snb-L^LG3!VN zra#C(zCD(nzg#W9GdCu8xw3AKce~cGd1CEy-PObPLv2HC!?!0$6NVED5~~L^p664u zsd>~aYOY9U+pxrh?>pSPGs)PoEZq)osr^Bk61g3YN3yFW)iW;6r|&bM`W~zzQ+CA< zYYfk&@3>m>j5MjGtG+OI(eN)Z&X?UbR%VR&{ob&7tOYtjTe9$@c*gTJ(%Kq!} zBd8JFh)XT*_5rIy3*6QoUf{9gVB7TvBmE;wM&6H@yhH1Cz0s@D{o~=^y&Ec2 z!zwAK7W>HUcfJ`@n|#<>>-y&7<&?uGY?hUuG{%K>Q)-3!Ec?77@=q*#dR=(sm9WNc z|1SS)!l4_FEBEP-T8^SdkKZgWy77CJ-;J|zrt)d>iQ5adhuU}5+^IQV(_Pb1bE~HL e#nHh02O4di+k4JW^@iWny(MI_X8yvDBL4!e={Exa literal 0 HcmV?d00001 diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V4.sqlite-shm b/Tests/BookmarksTests/Resources/Bookmarks_V4.sqlite-shm new file mode 100644 index 0000000000000000000000000000000000000000..0738e5302878e05a53647f04879dd37338387f63 GIT binary patch literal 32768 zcmeI*Jx&5)6a~-`9ON&=U?HU&a3}16P1w`ZvIsX|;|i>xCNZ&?Ew})Dj}T&OiSOLx zW+s!FuQ&_1v*@0iOfP!VH234U&Z6u0&-U>3@UeY){XWjS)#HA%d~2U}i$A^}N1c-A z9)IQh_;Q)XT|asoZKH$eU33^7Mf3YfR0t3tK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5;& zYZl1GBOy>zAh))JKuv*MnG*sv1#(|e2-FnFWl7{KwFoq7A_J>z~^8!$!+-b`9N>~0ppTSE)Qi{M3XwZ%k2(baUdc&dnq zmnznJ@X~{aNDp4B2tq3@m~I8NpdMlQJ4&VFA&IgvW5Esa6rhg)#A+XCaP{-Im-D~qt%!+{~l&;_j8?v`R;5n%;3>oetO{z z{V<{=8ND{l+_qM0A`4w{8DXCNW$b7+n-hAVZ>)UTN7K{OoAZ8OSbjO=ZETngt?5I_I{ z1Q0*~0R#|0009ILKwxMD5~H Date: Wed, 6 Dec 2023 10:52:40 +0000 Subject: [PATCH 16/39] Breakage report improvements (#578) Task/Issue URL: https://app.asana.com/0/1205842942115003/1205692741026215/f iOS PR: duckduckgo/iOS#2168 macOS PR: duckduckgo/macos-browser#1864 What kind of version bump will this require?: Major (Braking changes in privacy-dashboard) Description: The previous implementation of the Report Broken Site page was different between iOS and macOS, with this PR I have updated and aligned the PrivacyDashboardController so the web implementation of the Report Broken Site can be used in both iOS and macOS. The changes are mostly around delegates and which User script is handled for each platform. Splitting the delegate into 3 new delegates: PrivacyDashboardNavigationDelegate, PrivacyDashboardReportBrokenSiteDelegate, PrivacyDashboardControllerDelegate --- Package.resolved | 4 +- Package.swift | 2 +- .../Model/CookieConsentInfo.swift | 2 +- .../Model/ProtectionStatus.swift | 2 +- .../PrivacyDashboard/Model/TrackerInfo.swift | 10 +- .../PrivacyDashboardController.swift | 126 +++++++++++------- Sources/PrivacyDashboard/PrivacyInfo.swift | 2 +- .../PrivacyDashboardUserScript.swift | 76 +++++------ .../ViewModel/ServerTrustViewModel.swift | 52 ++++---- 9 files changed, 151 insertions(+), 125 deletions(-) diff --git a/Package.resolved b/Package.resolved index 450099eed..98582486d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "daa9708223b4b4318fb6448ca44801dfabcddc6f", - "version" : "3.0.0" + "revision" : "59dedf0f4ff1e9147de0806a54c6043861eb0870", + "version" : "3.1.0" } }, { diff --git a/Package.swift b/Package.swift index fa8c4e038..62ed4a886 100644 --- a/Package.swift +++ b/Package.swift @@ -37,8 +37,8 @@ let package = Package( .package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.1"), .package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"), .package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "2.1.0"), + .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "3.1.0" ), .package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "4.52.0"), - .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "3.0.0"), .package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"), .package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"), .package(url: "https://github.com/duckduckgo/wireguard-apple", exact: "1.1.1") diff --git a/Sources/PrivacyDashboard/Model/CookieConsentInfo.swift b/Sources/PrivacyDashboard/Model/CookieConsentInfo.swift index c86872932..475d24931 100644 --- a/Sources/PrivacyDashboard/Model/CookieConsentInfo.swift +++ b/Sources/PrivacyDashboard/Model/CookieConsentInfo.swift @@ -24,7 +24,7 @@ public struct CookieConsentInfo: Encodable { let optoutFailed: Bool? let selftestFailed: Bool? let configurable = true - + public init(consentManaged: Bool, cosmetic: Bool?, optoutFailed: Bool?, selftestFailed: Bool?) { self.consentManaged = consentManaged self.cosmetic = cosmetic diff --git a/Sources/PrivacyDashboard/Model/ProtectionStatus.swift b/Sources/PrivacyDashboard/Model/ProtectionStatus.swift index 22020c81b..2fc310faa 100644 --- a/Sources/PrivacyDashboard/Model/ProtectionStatus.swift +++ b/Sources/PrivacyDashboard/Model/ProtectionStatus.swift @@ -20,7 +20,7 @@ import Foundation public struct ProtectionStatus: Encodable { - + let unprotectedTemporary: Bool let enabledFeatures: [String] let allowlisted: Bool diff --git a/Sources/PrivacyDashboard/Model/TrackerInfo.swift b/Sources/PrivacyDashboard/Model/TrackerInfo.swift index 9782b349a..8b5b051b7 100644 --- a/Sources/PrivacyDashboard/Model/TrackerInfo.swift +++ b/Sources/PrivacyDashboard/Model/TrackerInfo.swift @@ -26,11 +26,11 @@ public struct TrackerInfo: Encodable { case requests case installedSurrogates } - + public private (set) var trackers = Set() private(set) var thirdPartyRequests = Set() public private(set) var installedSurrogates = Set() - + public init() { } // MARK: - Collecting detected elements @@ -43,12 +43,12 @@ public struct TrackerInfo: Encodable { public mutating func add(detectedThirdPartyRequest request: DetectedRequest) { thirdPartyRequests.insert(request) } - + public mutating func addInstalledSurrogateHost(_ host: String, for tracker: DetectedRequest, onPageWithURL url: URL) { guard tracker.pageUrl == url.absoluteString else { return } installedSurrogates.insert(host) } - + // MARK: - Helper accessors public var trackersBlocked: [DetectedRequest] { @@ -67,5 +67,5 @@ public struct TrackerInfo: Encodable { try container.encode(allRequests, forKey: .requests) try container.encode(installedSurrogates, forKey: .installedSurrogates) } - + } diff --git a/Sources/PrivacyDashboard/PrivacyDashboardController.swift b/Sources/PrivacyDashboard/PrivacyDashboardController.swift index 085302444..9c715d5ef 100644 --- a/Sources/PrivacyDashboard/PrivacyDashboardController.swift +++ b/Sources/PrivacyDashboard/PrivacyDashboardController.swift @@ -26,58 +26,77 @@ public enum PrivacyDashboardOpenSettingsTarget: String { case cookiePopupManagement = "cpm" } -public protocol PrivacyDashboardControllerDelegate: AnyObject { - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, - didChangeProtectionSwitch protectionState: ProtectionState) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestOpenUrlInNewTab url: URL) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, - didRequestOpenSettings target: PrivacyDashboardOpenSettingsTarget) - +/// Navigation delegate for the pages provided by the PrivacyDashboardController +public protocol PrivacyDashboardNavigationDelegate: AnyObject { #if os(iOS) func privacyDashboardControllerDidTapClose(_ privacyDashboardController: PrivacyDashboardController) - func privacyDashboardControllerDidRequestShowReportBrokenSite(_ privacyDashboardController: PrivacyDashboardController) #endif - -#if os(macOS) + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didSetHeight height: Int) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, +} + +/// `Report broken site` web page delegate +public protocol PrivacyDashboardReportBrokenSiteDelegate: AnyObject { + + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestSubmitBrokenSiteReportWithCategory category: String, description: String) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, - didSetPermission permissionName: String, - to state: PermissionAuthorizationState) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, - setPermission permissionName: String, - paused: Bool) -#endif + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + reportBrokenSiteDidChangeProtectionSwitch protectionState: ProtectionState) +} + +/// `Privacy Dasboard` web page delegate +public protocol PrivacyDashboardControllerDelegate: AnyObject { + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + didChangeProtectionSwitch protectionState: ProtectionState) + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + didRequestOpenUrlInNewTab url: URL) + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + didRequestOpenSettings target: PrivacyDashboardOpenSettingsTarget) + func privacyDashboardControllerDidRequestShowReportBrokenSite(_ privacyDashboardController: PrivacyDashboardController) + +#if os(macOS) + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + didSetPermission permissionName: String, to state: PermissionAuthorizationState) + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + setPermission permissionName: String, paused: Bool) +#endif } -@MainActor -public final class PrivacyDashboardController: NSObject { +/// This controller provides two type of user experiences +/// 1- `Privacy Dashboard` with the possibility to navigate to the `Report broken site` page +/// 2- Direct access to the `Report broken site` page +/// Which flow is used is decided at `setup(...)` time, where if `reportBrokenSiteOnly` is true then the `Report broken site` page is opened directly. +@MainActor public final class PrivacyDashboardController: NSObject { - public weak var delegate: PrivacyDashboardControllerDelegate? + // Delegates + public weak var privacyDashboardDelegate: PrivacyDashboardControllerDelegate? + public weak var privacyDashboardNavigationDelegate: PrivacyDashboardNavigationDelegate? + public weak var privacyDashboardReportBrokenSiteDelegate: PrivacyDashboardReportBrokenSiteDelegate? @Published public var theme: PrivacyDashboardTheme? public var preferredLocale: String? @Published public var allowedPermissions: [AllowedPermission] = [] - public private(set) weak var privacyInfo: PrivacyInfo? - private weak var webView: WKWebView? + private weak var webView: WKWebView? private let privacyDashboardScript = PrivacyDashboardUserScript() private var cancellables = Set() - + public init(privacyInfo: PrivacyInfo?) { self.privacyInfo = privacyInfo } - public func setup(for webView: WKWebView) { + /// Configure the webview for showing `Privacy Dasboard` or `Report broken site` + /// - Parameters: + /// - webView: The webview to use + /// - reportBrokenSiteOnly: true = `Report broken site`, false = `Privacy Dasboard` + public func setup(for webView: WKWebView, reportBrokenSiteOnly: Bool) { self.webView = webView - webView.navigationDelegate = self setupPrivacyDashboardUserScript() - loadPrivacyDashboardHTML() + loadPrivacyDashboardHTML(reportBrokenSiteOnly: reportBrokenSiteOnly) } public func updatePrivacyInfo(_ privacyInfo: PrivacyInfo?) { @@ -112,20 +131,25 @@ public final class PrivacyDashboardController: NSObject { privacyDashboardScript.delegate = self webView.configuration.userContentController.addUserScript(privacyDashboardScript.makeWKUserScriptSync()) - + privacyDashboardScript.messageNames.forEach { messageName in webView.configuration.userContentController.add(privacyDashboardScript, name: messageName) } } - private func loadPrivacyDashboardHTML() { - guard let url = Bundle.privacyDashboardURL else { return } + private func loadPrivacyDashboardHTML(reportBrokenSiteOnly: Bool) { + guard var url = Bundle.privacyDashboardURL else { return } + + if reportBrokenSiteOnly { + url = url.appendingParameter(name: "screen", value: ProtectionState.EventOriginScreen.breakageForm.rawValue) + } + webView?.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent().deletingLastPathComponent()) } } extension PrivacyDashboardController: WKNavigationDelegate { - + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { subscribeToDataModelChanges() @@ -155,7 +179,7 @@ extension PrivacyDashboardController: WKNavigationDelegate { }) .store(in: &cancellables) } - + private func subscribeToTrackerInfo() { privacyInfo?.$trackerInfo .receive(on: DispatchQueue.main) @@ -234,53 +258,55 @@ extension PrivacyDashboardController: WKNavigationDelegate { } extension PrivacyDashboardController: PrivacyDashboardUserScriptDelegate { - + func userScript(_ userScript: PrivacyDashboardUserScript, didRequestOpenSettings target: String) { let settingsTarget = PrivacyDashboardOpenSettingsTarget(rawValue: target) ?? .general - delegate?.privacyDashboardController(self, didRequestOpenSettings: settingsTarget) + privacyDashboardDelegate?.privacyDashboardController(self, didRequestOpenSettings: settingsTarget) } - + func userScript(_ userScript: PrivacyDashboardUserScript, didChangeProtectionState protectionState: ProtectionState) { - delegate?.privacyDashboardController(self, didChangeProtectionSwitch: protectionState) + + switch protectionState.eventOrigin.screen { + case .primaryScreen: + privacyDashboardDelegate?.privacyDashboardController(self, didChangeProtectionSwitch: protectionState) + case .breakageForm: + privacyDashboardReportBrokenSiteDelegate?.privacyDashboardController(self, reportBrokenSiteDidChangeProtectionSwitch: protectionState) + } + } func userScript(_ userScript: PrivacyDashboardUserScript, didRequestOpenUrlInNewTab url: URL) { - delegate?.privacyDashboardController(self, didRequestOpenUrlInNewTab: url) + privacyDashboardDelegate?.privacyDashboardController(self, didRequestOpenUrlInNewTab: url) } - + func userScriptDidRequestClosing(_ userScript: PrivacyDashboardUserScript) { #if os(iOS) - delegate?.privacyDashboardControllerDidTapClose(self) + privacyDashboardNavigationDelegate?.privacyDashboardControllerDidTapClose(self) #endif } func userScriptDidRequestShowReportBrokenSite(_ userScript: PrivacyDashboardUserScript) { -#if os(iOS) - delegate?.privacyDashboardControllerDidRequestShowReportBrokenSite(self) -#endif + privacyDashboardDelegate?.privacyDashboardControllerDidRequestShowReportBrokenSite(self) } func userScript(_ userScript: PrivacyDashboardUserScript, setHeight height: Int) { -#if os(macOS) - delegate?.privacyDashboardController(self, didSetHeight: height) -#endif + privacyDashboardNavigationDelegate?.privacyDashboardController(self, didSetHeight: height) } func userScript(_ userScript: PrivacyDashboardUserScript, didRequestSubmitBrokenSiteReportWithCategory category: String, description: String) { -#if os(macOS) - delegate?.privacyDashboardController(self, didRequestSubmitBrokenSiteReportWithCategory: category, description: description) -#endif + privacyDashboardReportBrokenSiteDelegate?.privacyDashboardController(self, didRequestSubmitBrokenSiteReportWithCategory: category, + description: description) } func userScript(_ userScript: PrivacyDashboardUserScript, didSetPermission permission: String, to state: PermissionAuthorizationState) { #if os(macOS) - delegate?.privacyDashboardController(self, didSetPermission: permission, to: state) + privacyDashboardDelegate?.privacyDashboardController(self, didSetPermission: permission, to: state) #endif } func userScript(_ userScript: PrivacyDashboardUserScript, setPermission permission: String, paused: Bool) { #if os(macOS) - delegate?.privacyDashboardController(self, setPermission: permission, paused: paused) + privacyDashboardDelegate?.privacyDashboardController(self, setPermission: permission, paused: paused) #endif } } diff --git a/Sources/PrivacyDashboard/PrivacyInfo.swift b/Sources/PrivacyDashboard/PrivacyInfo.swift index 41d937793..fd488e382 100644 --- a/Sources/PrivacyDashboard/PrivacyInfo.swift +++ b/Sources/PrivacyDashboard/PrivacyInfo.swift @@ -35,7 +35,7 @@ public final class PrivacyInfo { self.url = url self.parentEntity = parentEntity self.protectionStatus = protectionStatus - + trackerInfo = TrackerInfo() } diff --git a/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift b/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift index 77b9c5c5e..1df481f4d 100644 --- a/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift +++ b/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift @@ -42,11 +42,11 @@ public enum PrivacyDashboardTheme: String, Encodable { public struct ProtectionState: Decodable { public let isProtected: Bool public let eventOrigin: EventOrigin - + public struct EventOrigin: Decodable { public let screen: EventOriginScreen } - + public enum EventOriginScreen: String, Decodable { case primaryScreen case breakageForm @@ -54,7 +54,7 @@ public struct ProtectionState: Decodable { } final class PrivacyDashboardUserScript: NSObject, StaticUserScript { - + enum MessageNames: String, CaseIterable { case privacyDashboardSetProtection case privacyDashboardSetSize @@ -66,21 +66,21 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { case privacyDashboardSetPermission case privacyDashboardSetPermissionPaused } - + static var injectionTime: WKUserScriptInjectionTime { .atDocumentStart } static var forMainFrameOnly: Bool { false } static var source: String = "" static var script: WKUserScript = PrivacyDashboardUserScript.makeWKUserScript() var messageNames: [String] { MessageNames.allCases.map(\.rawValue) } - + weak var delegate: PrivacyDashboardUserScriptDelegate? - + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let messageType = MessageNames(rawValue: message.name) else { assertionFailure("PrivacyDashboardUserScript: unexpected message name \(message.name)") return } - + switch messageType { case .privacyDashboardSetProtection: handleSetProtection(message: message) @@ -104,14 +104,14 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { } // MARK: - JS message handlers - + private func handleSetProtection(message: WKScriptMessage) { - + guard let protectionState: ProtectionState = DecodableHelper.decode(from: message.messageBody) else { assertionFailure("privacyDashboardSetProtection: expected ProtectionState") return } - + delegate?.userScript(self, didChangeProtectionState: protectionState) } @@ -121,10 +121,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("privacyDashboardSetHeight: expected height to be an Int") return } - + delegate?.userScript(self, setHeight: height) } - + private func handleClose() { delegate?.userScriptDidRequestClosing(self) } @@ -140,7 +140,7 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("privacyDashboardSetHeight: expected { category: String, description: String }") return } - + delegate?.userScript(self, didRequestSubmitBrokenSiteReportWithCategory: category, description: description) } @@ -152,10 +152,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("handleOpenUrlInNewTab: expected { url: '...' } ") return } - + delegate?.userScript(self, didRequestOpenUrlInNewTab: url) } - + private func handleOpenSettings(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let target = dict["target"] as? String @@ -163,10 +163,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("handleOpenSettings: expected { target: '...' } ") return } - + delegate?.userScript(self, didRequestOpenSettings: target) } - + private func handleSetPermission(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let permission = dict["permission"] as? String, @@ -175,10 +175,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("privacyDashboardSetPermission: expected { permission: PermissionType, value: PermissionAuthorizationState }") return } - + delegate?.userScript(self, didSetPermission: permission, to: state) } - + private func handleSetPermissionPaused(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let permission = dict["permission"] as? String, @@ -187,10 +187,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("handleSetPermissionPaused: expected { permission: PermissionType, paused: Bool }") return } - + delegate?.userScript(self, setPermission: permission, paused: paused) } - + // MARK: - Calls to script's JS API func setTrackerInfo(_ tabUrl: URL, trackerInfo: TrackerInfo, webView: WKWebView) { @@ -198,15 +198,15 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("Can't encode trackerInfoViewModel into JSON") return } - + guard let safeTabUrl = try? JSONEncoder().encode(tabUrl).utf8String() else { assertionFailure("Can't encode tabUrl into JSON") return } - + evaluate(js: "window.onChangeRequestData(\(safeTabUrl), \(trackerBlockingDataJson))", in: webView) } - + func setProtectionStatus(_ protectionStatus: ProtectionStatus, webView: WKWebView) { guard let protectionStatusJson = try? JSONEncoder().encode(protectionStatus).utf8String() else { assertionFailure("Can't encode mockProtectionStatus into JSON") @@ -215,42 +215,42 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { evaluate(js: "window.onChangeProtectionStatus(\(protectionStatusJson))", in: webView) } - + func setUpgradedHttps(_ upgradedHttps: Bool, webView: WKWebView) { evaluate(js: "window.onChangeUpgradedHttps(\(upgradedHttps))", in: webView) } - + func setParentEntity(_ parentEntity: Entity?, webView: WKWebView) { if parentEntity == nil { return } - + guard let parentEntityJson = try? JSONEncoder().encode(parentEntity).utf8String() else { assertionFailure("Can't encode parentEntity into JSON") return } - + evaluate(js: "window.onChangeParentEntity(\(parentEntityJson))", in: webView) } - + func setTheme(_ theme: PrivacyDashboardTheme?, webView: WKWebView) { if theme == nil { return } - + guard let themeJson = try? JSONEncoder().encode(theme).utf8String() else { assertionFailure("Can't encode themeName into JSON") return } - + evaluate(js: "window.onChangeTheme(\(themeJson))", in: webView) } - + func setServerTrust(_ serverTrustViewModel: ServerTrustViewModel, webView: WKWebView) { guard let certificateDataJson = try? JSONEncoder().encode(serverTrustViewModel).utf8String() else { assertionFailure("Can't encode serverTrustViewModel into JSON") return } - + evaluate(js: "window.onChangeCertificateData(\(certificateDataJson))", in: webView) } - + func setIsPendingUpdates(_ isPendingUpdates: Bool, webView: WKWebView) { evaluate(js: "window.onIsPendingUpdates(\(isPendingUpdates))", in: webView) } @@ -283,17 +283,17 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { self.evaluate(js: "window.onChangeAllowedPermissions(\(allowedPermissionsJson))", in: webView) } - + private func evaluate(js: String, in webView: WKWebView) { webView.evaluateJavaScript(js) } - + } extension Data { - + func utf8String() -> String? { return String(data: self, encoding: .utf8) } - + } diff --git a/Sources/PrivacyDashboard/ViewModel/ServerTrustViewModel.swift b/Sources/PrivacyDashboard/ViewModel/ServerTrustViewModel.swift index eb94d1db3..0862cb89d 100644 --- a/Sources/PrivacyDashboard/ViewModel/ServerTrustViewModel.swift +++ b/Sources/PrivacyDashboard/ViewModel/ServerTrustViewModel.swift @@ -19,28 +19,28 @@ import Foundation public struct ServerTrustViewModel: Encodable { - + struct SecCertificateViewModel: Encodable { - + let summary: String? let commonName: String? let emails: [String]? let publicKey: SecKeyViewModel? - + public init(secCertificate: SecCertificate) { summary = SecCertificateCopySubjectSummary(secCertificate) as String? ?? "" - + var commonName: CFString? SecCertificateCopyCommonName(secCertificate, &commonName) self.commonName = commonName as String? ?? "" - + var emails: CFArray? if errSecSuccess == SecCertificateCopyEmailAddresses(secCertificate, &emails) { self.emails = emails as? [String] } else { self.emails = nil } - + var secTrust: SecTrust? if errSecSuccess == SecTrustCreateWithCertificates(secCertificate, SecPolicyCreateBasicX509(), &secTrust), let certTrust = secTrust { if #available(iOS 14.0, macOS 11.0, *) { @@ -53,11 +53,11 @@ public struct ServerTrustViewModel: Encodable { publicKey = nil } } - + } - + struct SecKeyViewModel: Encodable { - + static func typeToString(_ type: String) -> String? { switch type as CFString { case kSecAttrKeyTypeRSA: return "RSA" @@ -66,14 +66,14 @@ public struct ServerTrustViewModel: Encodable { default: return nil } } - + let keyId: Data? let externalRepresentation: Data? - + let bitSize: Int? let blockSize: Int? let effectiveSize: Int? - + let canDecrypt: Bool let canDerive: Bool let canEncrypt: Bool @@ -81,20 +81,20 @@ public struct ServerTrustViewModel: Encodable { let canUnwrap: Bool let canVerify: Bool let canWrap: Bool - + let isPermanent: Bool? let type: String? - + init?(secKey: SecKey?) { guard let secKey = secKey else { return nil } - + blockSize = SecKeyGetBlockSize(secKey) externalRepresentation = SecKeyCopyExternalRepresentation(secKey, nil) as Data? - + let attrs: NSDictionary? = SecKeyCopyAttributes(secKey) - + bitSize = attrs?[kSecAttrKeySizeInBits] as? Int effectiveSize = attrs?[kSecAttrEffectiveKeySize] as? Int canDecrypt = attrs?[kSecAttrCanDecrypt] as? Bool ?? false @@ -106,36 +106,36 @@ public struct ServerTrustViewModel: Encodable { canWrap = attrs?[kSecAttrCanWrap] as? Bool ?? false isPermanent = attrs?[kSecAttrIsPermanent] as? Bool ?? false keyId = attrs?[kSecAttrApplicationLabel] as? Data - + if let type = attrs?[kSecAttrType] as? String { self.type = Self.typeToString(type) } else { self.type = nil } - + } - + } - + let secCertificateViewModels: [SecCertificateViewModel] - + public init?(serverTrust: SecTrust?) { guard let serverTrust = serverTrust else { return nil } - + let secTrust = serverTrust let count = SecTrustGetCertificateCount(secTrust) guard count != 0 else { return nil } - + var secCertificateViewModels = [SecCertificateViewModel]() for i in 0 ..< count { guard let certificate = SecTrustGetCertificateAtIndex(secTrust, i) else { return nil } let certificateViewModel = SecCertificateViewModel(secCertificate: certificate) secCertificateViewModels.append(certificateViewModel) } - + self.secCertificateViewModels = secCertificateViewModels } - + } From 822e284a50ebb42f0880ebcae87dc36e007f8c31 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Wed, 6 Dec 2023 11:32:57 +0000 Subject: [PATCH 17/39] privacy-dashboard 3.1.1 (#590) --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 98582486d..b98be37b5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "59dedf0f4ff1e9147de0806a54c6043861eb0870", - "version" : "3.1.0" + "revision" : "38336a574e13090764ba09a6b877d15ee514e371", + "version" : "3.1.1" } }, { diff --git a/Package.swift b/Package.swift index 62ed4a886..1869fa4d7 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( .package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.1"), .package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"), .package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "2.1.0"), - .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "3.1.0" ), + .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "3.1.1" ), .package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "4.52.0"), .package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"), .package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"), From c871e62fd8d07f9e3136948614003fbe7e582963 Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Wed, 6 Dec 2023 09:06:55 -0600 Subject: [PATCH 18/39] Update TRK (#580) * Update TRK * Update TRK * Pin TRK version --- Package.resolved | 8 ++++---- Package.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Package.resolved b/Package.resolved index b98be37b5..ca65c3395 100644 --- a/Package.resolved +++ b/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", - "version" : "0.5.0" + "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", + "version" : "1.2.3" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/TrackerRadarKit", "state" : { - "revision" : "4684440d03304e7638a2c8086895367e90987463", - "version" : "1.2.1" + "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", + "version" : "1.2.2" } }, { diff --git a/Package.swift b/Package.swift index 1869fa4d7..1255ac8fd 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "10.0.0"), .package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.2.0"), - .package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.1"), + .package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.2"), .package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"), .package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "2.1.0"), .package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "3.1.1" ), From 11a5fb5e7bf1f4fb4e2a79ce8dbe2eb39b583495 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 6 Dec 2023 18:51:57 +0100 Subject: [PATCH 19/39] Add errorEvent handling to location list repo (#593) * Add errorEvent handling to location list repo * swiftlint ya bass * Away with you, swiftlint --- ...workProtectionLocationListRepository.swift | 20 ++++++++-- ...LocationListCompositeRepositoryTests.swift | 37 ++++++++++++++++++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift b/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift index 773aac8df..207fe5020 100644 --- a/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift +++ b/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift @@ -17,6 +17,7 @@ // import Foundation +import Common public protocol NetworkProtectionLocationListRepository { func fetchLocationList() async throws -> [NetworkProtectionLocation] @@ -26,17 +27,24 @@ final public class NetworkProtectionLocationListCompositeRepository: NetworkProt @MainActor private static var locationList: [NetworkProtectionLocation] = [] private let client: NetworkProtectionClient private let tokenStore: NetworkProtectionTokenStore + private let errorEvents: EventMapping - convenience public init(environment: VPNSettings.SelectedEnvironment, tokenStore: NetworkProtectionTokenStore) { + convenience public init(environment: VPNSettings.SelectedEnvironment, + tokenStore: NetworkProtectionTokenStore, + errorEvents: EventMapping) { self.init( client: NetworkProtectionBackendClient(environment: environment), - tokenStore: tokenStore + tokenStore: tokenStore, + errorEvents: errorEvents ) } - init(client: NetworkProtectionClient, tokenStore: NetworkProtectionTokenStore) { + init(client: NetworkProtectionClient, + tokenStore: NetworkProtectionTokenStore, + errorEvents: EventMapping) { self.client = client self.tokenStore = tokenStore + self.errorEvents = errorEvents } @MainActor @@ -50,11 +58,15 @@ final public class NetworkProtectionLocationListCompositeRepository: NetworkProt } Self.locationList = try await client.getLocations(authToken: authToken).get() } catch let error as NetworkProtectionErrorConvertible { + errorEvents.fire(error.networkProtectionError) throw error.networkProtectionError } catch let error as NetworkProtectionError { + errorEvents.fire(error) throw error } catch { - throw NetworkProtectionError.unhandledError(function: #function, line: #line, error: error) + let unhandledError = NetworkProtectionError.unhandledError(function: #function, line: #line, error: error) + errorEvents.fire(unhandledError) + throw unhandledError } return Self.locationList } diff --git a/Tests/NetworkProtectionTests/Repositories/NetworkProtectionLocationListCompositeRepositoryTests.swift b/Tests/NetworkProtectionTests/Repositories/NetworkProtectionLocationListCompositeRepositoryTests.swift index 33317e305..71a392d26 100644 --- a/Tests/NetworkProtectionTests/Repositories/NetworkProtectionLocationListCompositeRepositoryTests.swift +++ b/Tests/NetworkProtectionTests/Repositories/NetworkProtectionLocationListCompositeRepositoryTests.swift @@ -21,17 +21,24 @@ import Foundation import XCTest @testable import NetworkProtection @testable import NetworkProtectionTestUtils +import Common class NetworkProtectionLocationListCompositeRepositoryTests: XCTestCase { var repository: NetworkProtectionLocationListCompositeRepository! var client: MockNetworkProtectionClient! var tokenStore: MockNetworkProtectionTokenStorage! + var verifyErrorEvent: ((NetworkProtectionError) -> Void)? override func setUp() { super.setUp() client = MockNetworkProtectionClient() tokenStore = MockNetworkProtectionTokenStorage() - repository = NetworkProtectionLocationListCompositeRepository(client: client, tokenStore: tokenStore) + repository = NetworkProtectionLocationListCompositeRepository( + client: client, + tokenStore: tokenStore, + errorEvents: .init { [weak self] event, _, _, _ in + self?.verifyErrorEvent?(event) + }) } @MainActor @@ -93,6 +100,23 @@ class NetworkProtectionLocationListCompositeRepositoryTests: XCTestCase { } } + func testFetchLocationList_noAuthToken_sendsErrorEvent() async { + client.stubGetLocations = .success([.testData()]) + tokenStore.stubFetchToken = nil + var didReceiveError: Bool = false + verifyErrorEvent = { error in + didReceiveError = true + switch error { + case .noAuthTokenFound: + break + default: + XCTFail("Expected noAuthTokenFound error") + } + } + _ = try? await repository.fetchLocationList() + XCTAssertTrue(didReceiveError) + } + func testFetchLocationList_fetchThrows_throwsError() async throws { client.stubGetLocations = .failure(.failedToFetchLocationList(nil)) var errorResult: Error? @@ -104,6 +128,17 @@ class NetworkProtectionLocationListCompositeRepositoryTests: XCTestCase { XCTAssertNotNil(errorResult) } + + func testFetchLocationList_fetchThrows_sendsErrorEvent() async { + client.stubGetLocations = .failure(.failedToFetchLocationList(nil)) + var didReceiveError: Bool = false + verifyErrorEvent = { _ in + didReceiveError = true + // Matching errors is not working for some reason, so just checking for any error + } + _ = try? await repository.fetchLocationList() + XCTAssertTrue(didReceiveError) + } } private extension NetworkProtectionLocation { From fe577c508ad4ea163075ac8fab20673d0a7565f6 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 6 Dec 2023 20:53:22 -0300 Subject: [PATCH 20/39] No longer excluding the 10.0.0.0/8 range (#594) Task/Issue URL: https://app.asana.com/0/0/1206114617364818/f iOS PR: duckduckgo/iOS#2239 macOS PR: duckduckgo/macos-browser#1934 Description We no longer exclude 10.0.0.0/8 from the VPN since it was causing our DNS to be excluded as well. --- Sources/NetworkProtection/Settings/RoutingRange.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/NetworkProtection/Settings/RoutingRange.swift b/Sources/NetworkProtection/Settings/RoutingRange.swift index 04426f9a2..4f9eed423 100644 --- a/Sources/NetworkProtection/Settings/RoutingRange.swift +++ b/Sources/NetworkProtection/Settings/RoutingRange.swift @@ -24,7 +24,11 @@ public enum RoutingRange { public static let alwaysExcludedIPv4Ranges: [RoutingRange] = [ .section("IPv4 - Always Excluded"), - .range("10.0.0.0/8" /* 255.0.0.0 */, description: "disabled for enforceRoutes"), + // This is disabled because excluded routes seem to trump included routes, and our DNS + // server's IP address lives in this range. + // Ref: https://app.asana.com/0/1203708860857015/1206099277258514/f + // + // .range("10.0.0.0/8" /* 255.0.0.0 */, description: "disabled for enforceRoutes"), .range("100.64.0.0/16" /* 255.255.0.0 */, description: "Shared Address Space"), .range("127.0.0.0/8" /* 255.0.0.0 */, description: "Loopback"), .range("169.254.0.0/16" /* 255.255.0.0 */, description: "Link-local"), From f494273b5c2a2b4929339ed0cf63a6674abe7e54 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Dec 2023 12:11:06 +0100 Subject: [PATCH 21/39] Ensure that LinkPresentation framework is called on main thread (#595) Task/Issue URL: https://app.asana.com/0/1201493110486074/1206113035171145/f Description: Ensure that LinkPresentation's metadata lookup API is called on main thread. Otherwise, it would keep crashing on iOS 15. --- Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift b/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift index bd2a96d73..970fed75d 100644 --- a/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift +++ b/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift @@ -31,7 +31,7 @@ public final class FaviconFetcher: NSObject, FaviconFetching { let metadataFetcher = LPMetadataProvider() // Allow LinkPresentation to fail so that we can fall back to fetching hardcoded paths - let metadata: LPLinkMetadata? = await { + let metadata: LPLinkMetadata? = await { @MainActor in if #available(iOS 15.0, macOS 12.0, *) { var request = URLRequest(url: url) request.attribution = .user From 5ca0b3d915ab73de43744db40edf41c1d5060034 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Dec 2023 12:14:05 +0100 Subject: [PATCH 22/39] Implement deleteAccount Sync endpoint (#596) Task/Issue URL: https://app.asana.com/0/1201493110486074/1206055891122348/f Description: Add missing delete-account endpoint and update implementation to call it instead of logging out all devices. --- Sources/DDGSync/internal/AccountManager.swift | 15 +++++---------- Sources/DDGSync/internal/Endpoints.swift | 5 ++++- .../RemoteAPIRequestCreatingExtensions.swift | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Sources/DDGSync/internal/AccountManager.swift b/Sources/DDGSync/internal/AccountManager.swift index 2469372d6..bcec3d38d 100644 --- a/Sources/DDGSync/internal/AccountManager.swift +++ b/Sources/DDGSync/internal/AccountManager.swift @@ -143,17 +143,12 @@ struct AccountManager: AccountManaging { throw SyncError.noToken } - let devices = try await fetchDevicesForAccount(account) - - // Logout other devices first or the call will fail - for device in devices.filter({ $0.id != account.deviceId }) { - try await logout(deviceId: device.id, token: token) - } + let request = api.createAuthenticatedJSONRequest(url: endpoints.deleteAccount, method: .POST, authToken: token) + let result = try await request.execute() + let statusCode = result.response.statusCode - // This is the last device, the backend will perge the data after this - // An explicit delete account endpoint might be better though - if let thisDevice = devices.first(where: { $0.id == account.deviceId }) { - try await logout(deviceId: thisDevice.id, token: token) + guard statusCode == 204 else { + throw SyncError.unexpectedStatusCode(statusCode) } } diff --git a/Sources/DDGSync/internal/Endpoints.swift b/Sources/DDGSync/internal/Endpoints.swift index 137506aa3..161394dad 100644 --- a/Sources/DDGSync/internal/Endpoints.swift +++ b/Sources/DDGSync/internal/Endpoints.swift @@ -23,9 +23,10 @@ class Endpoints { private(set) var baseURL: URL private(set) var signup: URL + private(set) var connect: URL private(set) var login: URL private(set) var logoutDevice: URL - private(set) var connect: URL + private(set) var deleteAccount: URL private(set) var syncGet: URL private(set) var syncPatch: URL @@ -47,6 +48,7 @@ class Endpoints { signup = baseURL.appendingPathComponent("sync/signup") login = baseURL.appendingPathComponent("sync/login") logoutDevice = baseURL.appendingPathComponent("sync/logout-device") + deleteAccount = baseURL.appendingPathComponent("sync/delete-account") connect = baseURL.appendingPathComponent("sync/connect") syncGet = baseURL.appendingPathComponent("sync") @@ -63,6 +65,7 @@ extension Endpoints { signup = baseURL.appendingPathComponent("sync/signup") login = baseURL.appendingPathComponent("sync/login") logoutDevice = baseURL.appendingPathComponent("sync/logout-device") + deleteAccount = baseURL.appendingPathComponent("sync/delete-account") connect = baseURL.appendingPathComponent("sync/connect") syncGet = baseURL.appendingPathComponent("sync") diff --git a/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift b/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift index fd196fdbc..3149d5339 100644 --- a/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift +++ b/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift @@ -40,7 +40,7 @@ extension RemoteAPIRequestCreating { func createAuthenticatedJSONRequest(url: URL, method: HTTPRequestMethod, authToken: String, - json: Data, + json: Data? = nil, headers: [String: String] = [:], parameters: [String: String] = [:]) -> HTTPRequesting { var headers = headers From 33e55105acc9d6d69ae1326fd5c506cefb89d5cc Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:44:10 +0000 Subject: [PATCH 23/39] Update autofill to 10.0.1 (#591) Task/Issue URL: https://app.asana.com/0/1206111598841006/1206111598841006 Autofill Release: https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/10.0.1 Description Updates Autofill to version 10.0.1. --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index ca65c3395..8482cdadb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "93677cc02cfe650ce7f417246afd0e8e972cd83e", - "version" : "10.0.0" + "revision" : "dbecae0df07650a21b5632a92fa2e498c96af7b5", + "version" : "10.0.1" } }, { diff --git a/Package.swift b/Package.swift index 1255ac8fd..df391ce3e 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "SecureStorage", targets: ["SecureStorage"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "10.0.0"), + .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "10.0.1"), .package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.2.0"), .package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.2"), .package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"), From cbadef1421d41793f430bb740645e1be85418267 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Wed, 6 Dec 2023 20:53:22 -0300 Subject: [PATCH 24/39] No longer excluding the 10.0.0.0/8 range (#594) Task/Issue URL: https://app.asana.com/0/0/1206114617364818/f iOS PR: duckduckgo/iOS#2239 macOS PR: duckduckgo/macos-browser#1934 Description We no longer exclude 10.0.0.0/8 from the VPN since it was causing our DNS to be excluded as well. --- Sources/NetworkProtection/Settings/RoutingRange.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/NetworkProtection/Settings/RoutingRange.swift b/Sources/NetworkProtection/Settings/RoutingRange.swift index 04426f9a2..4f9eed423 100644 --- a/Sources/NetworkProtection/Settings/RoutingRange.swift +++ b/Sources/NetworkProtection/Settings/RoutingRange.swift @@ -24,7 +24,11 @@ public enum RoutingRange { public static let alwaysExcludedIPv4Ranges: [RoutingRange] = [ .section("IPv4 - Always Excluded"), - .range("10.0.0.0/8" /* 255.0.0.0 */, description: "disabled for enforceRoutes"), + // This is disabled because excluded routes seem to trump included routes, and our DNS + // server's IP address lives in this range. + // Ref: https://app.asana.com/0/1203708860857015/1206099277258514/f + // + // .range("10.0.0.0/8" /* 255.0.0.0 */, description: "disabled for enforceRoutes"), .range("100.64.0.0/16" /* 255.255.0.0 */, description: "Shared Address Space"), .range("127.0.0.0/8" /* 255.0.0.0 */, description: "Loopback"), .range("169.254.0.0/16" /* 255.255.0.0 */, description: "Link-local"), From cd89124fd97d4b1125eb175d8f43130532c8ee49 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Dec 2023 12:11:06 +0100 Subject: [PATCH 25/39] Ensure that LinkPresentation framework is called on main thread (#595) Task/Issue URL: https://app.asana.com/0/1201493110486074/1206113035171145/f Description: Ensure that LinkPresentation's metadata lookup API is called on main thread. Otherwise, it would keep crashing on iOS 15. --- Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift b/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift index bd2a96d73..970fed75d 100644 --- a/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift +++ b/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift @@ -31,7 +31,7 @@ public final class FaviconFetcher: NSObject, FaviconFetching { let metadataFetcher = LPMetadataProvider() // Allow LinkPresentation to fail so that we can fall back to fetching hardcoded paths - let metadata: LPLinkMetadata? = await { + let metadata: LPLinkMetadata? = await { @MainActor in if #available(iOS 15.0, macOS 12.0, *) { var request = URLRequest(url: url) request.attribution = .user From 450f2b44dec9c42ee416f6b73eee59d3381ee470 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Dec 2023 12:14:05 +0100 Subject: [PATCH 26/39] Implement deleteAccount Sync endpoint (#596) Task/Issue URL: https://app.asana.com/0/1201493110486074/1206055891122348/f Description: Add missing delete-account endpoint and update implementation to call it instead of logging out all devices. --- Sources/DDGSync/internal/AccountManager.swift | 15 +++++---------- Sources/DDGSync/internal/Endpoints.swift | 5 ++++- .../RemoteAPIRequestCreatingExtensions.swift | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Sources/DDGSync/internal/AccountManager.swift b/Sources/DDGSync/internal/AccountManager.swift index 2469372d6..bcec3d38d 100644 --- a/Sources/DDGSync/internal/AccountManager.swift +++ b/Sources/DDGSync/internal/AccountManager.swift @@ -143,17 +143,12 @@ struct AccountManager: AccountManaging { throw SyncError.noToken } - let devices = try await fetchDevicesForAccount(account) - - // Logout other devices first or the call will fail - for device in devices.filter({ $0.id != account.deviceId }) { - try await logout(deviceId: device.id, token: token) - } + let request = api.createAuthenticatedJSONRequest(url: endpoints.deleteAccount, method: .POST, authToken: token) + let result = try await request.execute() + let statusCode = result.response.statusCode - // This is the last device, the backend will perge the data after this - // An explicit delete account endpoint might be better though - if let thisDevice = devices.first(where: { $0.id == account.deviceId }) { - try await logout(deviceId: thisDevice.id, token: token) + guard statusCode == 204 else { + throw SyncError.unexpectedStatusCode(statusCode) } } diff --git a/Sources/DDGSync/internal/Endpoints.swift b/Sources/DDGSync/internal/Endpoints.swift index 137506aa3..161394dad 100644 --- a/Sources/DDGSync/internal/Endpoints.swift +++ b/Sources/DDGSync/internal/Endpoints.swift @@ -23,9 +23,10 @@ class Endpoints { private(set) var baseURL: URL private(set) var signup: URL + private(set) var connect: URL private(set) var login: URL private(set) var logoutDevice: URL - private(set) var connect: URL + private(set) var deleteAccount: URL private(set) var syncGet: URL private(set) var syncPatch: URL @@ -47,6 +48,7 @@ class Endpoints { signup = baseURL.appendingPathComponent("sync/signup") login = baseURL.appendingPathComponent("sync/login") logoutDevice = baseURL.appendingPathComponent("sync/logout-device") + deleteAccount = baseURL.appendingPathComponent("sync/delete-account") connect = baseURL.appendingPathComponent("sync/connect") syncGet = baseURL.appendingPathComponent("sync") @@ -63,6 +65,7 @@ extension Endpoints { signup = baseURL.appendingPathComponent("sync/signup") login = baseURL.appendingPathComponent("sync/login") logoutDevice = baseURL.appendingPathComponent("sync/logout-device") + deleteAccount = baseURL.appendingPathComponent("sync/delete-account") connect = baseURL.appendingPathComponent("sync/connect") syncGet = baseURL.appendingPathComponent("sync") diff --git a/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift b/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift index fd196fdbc..3149d5339 100644 --- a/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift +++ b/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift @@ -40,7 +40,7 @@ extension RemoteAPIRequestCreating { func createAuthenticatedJSONRequest(url: URL, method: HTTPRequestMethod, authToken: String, - json: Data, + json: Data? = nil, headers: [String: String] = [:], parameters: [String: String] = [:]) -> HTTPRequesting { var headers = headers From 6d67e41feb5d7d22fec40fcede6b82eb88673900 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:06:38 -0500 Subject: [PATCH 27/39] Report NetP connection attempts, tunnel failures, and latency (#584) --- .../NetworkProtectionLatencyMonitor.swift | 281 ++++++++++++++++++ .../NetworkProtectionLatencyReporter.swift | 143 --------- ...etworkProtectionTunnelFailureMonitor.swift | 197 ++++++++++++ .../PacketTunnelProvider.swift | 137 +++++++-- .../Status/ConnectionStatus.swift | 19 ++ .../WireGuardKit/WireGuardAdapter.swift | 28 ++ .../ExponentialGeometricAverageTests.swift | 31 ++ ...NetworkProtectionLatencyMonitorTests.swift | 102 +++++++ 8 files changed, 765 insertions(+), 173 deletions(-) create mode 100644 Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyMonitor.swift delete mode 100644 Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyReporter.swift create mode 100644 Sources/NetworkProtection/Diagnostics/NetworkProtectionTunnelFailureMonitor.swift create mode 100644 Tests/NetworkProtectionTests/ExponentialGeometricAverageTests.swift create mode 100644 Tests/NetworkProtectionTests/NetworkProtectionLatencyMonitorTests.swift diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyMonitor.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyMonitor.swift new file mode 100644 index 000000000..7a582dfac --- /dev/null +++ b/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyMonitor.swift @@ -0,0 +1,281 @@ +// +// NetworkProtectionTunnelFailureMonitor.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 Network +import Common +import Combine + +final public class NetworkProtectionLatencyMonitor { + public enum ConnectionQuality: String { + case terrible + case poor + case moderate + case good + case excellent + case unknown + + init(average: TimeInterval) { + switch average { + case 300...: + self = .terrible + case 200..<300: + self = .poor + case 50..<200: + self = .moderate + case 20..<50: + self = .good + case 0..<20: + self = .excellent + default: + self = .unknown + } + } + } + + public enum Result { + case error + case quality(ConnectionQuality) + } + + private static let reportThreshold: TimeInterval = .minutes(10) + private static let measurementInterval: TimeInterval = .seconds(5) + private static let pingTimeout: TimeInterval = 0.3 + + private static let unknownLatency: TimeInterval = -1 + + public var publisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + private let subject = PassthroughSubject() + + private let latencySubject = PassthroughSubject() + private var latencyCancellable: AnyCancellable? + + private actor TimerRunCoordinator { + private(set) var isRunning = false + + func start() { + isRunning = true + } + + func stop() { + isRunning = false + } + } + + private var timer: DispatchSourceTimer? + private let timerRunCoordinator = TimerRunCoordinator() + private let timerQueue: DispatchQueue + + private let lock = NSLock() + + private var _lastLatencyReported: Date = .distantPast + private(set) var lastLatencyReported: Date { + get { + lock.lock(); defer { lock.unlock() } + return _lastLatencyReported + } + set { + lock.lock() + self._lastLatencyReported = newValue + lock.unlock() + } + } + + private let serverIP: () -> IPv4Address? + + private let log: OSLog + + private var _ignoreThreshold = false + private(set) var ignoreThreshold: Bool { + get { + lock.lock(); defer { lock.unlock() } + return _ignoreThreshold + } + set { + lock.lock() + self._ignoreThreshold = newValue + lock.unlock() + } + } + + // MARK: - Init & deinit + + init(serverIP: @escaping () -> IPv4Address?, timerQueue: DispatchQueue, log: OSLog) { + self.serverIP = serverIP + self.timerQueue = timerQueue + self.log = log + + os_log("[+] %{public}@", log: .networkProtectionMemoryLog, type: .debug, String(describing: self)) + } + + deinit { + os_log("[-] %{public}@", log: .networkProtectionMemoryLog, type: .debug, String(describing: self)) + + cancelTimerImmediately() + } + + // MARK: - Start/Stop monitoring + + public func start() async throws { + guard await !timerRunCoordinator.isRunning else { + os_log("Will not start the latency monitor as it's already running", log: log) + return + } + + os_log("⚫️ Starting latency monitor", log: log) + + latencyCancellable = latencySubject.eraseToAnyPublisher() + .scan(ExponentialGeometricAverage()) { [weak self] measurements, latency in + if latency >= 0 { + measurements.addMeasurement(latency) + os_log("⚫️ Latency: %{public}f milliseconds", log: .networkProtectionPixel, type: .debug, latency) + } else { + self?.subject.send(.error) + } + + os_log("⚫️ Average: %{public}f milliseconds", log: .networkProtectionPixel, type: .debug, measurements.average) + + return measurements + } + .map { ConnectionQuality(average: $0.average) } + .sink { [weak self] quality in + let now = Date() + if let self, + (now.timeIntervalSince1970 - self.lastLatencyReported.timeIntervalSince1970 >= Self.reportThreshold) || ignoreThreshold { + self.subject.send(.quality(quality)) + self.lastLatencyReported = now + } + } + + do { + try await scheduleTimer() + } catch { + os_log("⚫️ Stopping latency monitor prematurely", log: log) + throw error + } + } + + public func stop() async { + os_log("⚫️ Stopping latency monitor", log: log) + await stopScheduledTimer() + } + + // MARK: - Timer scheduling + + private func scheduleTimer() async throws { + await stopScheduledTimer() + + await timerRunCoordinator.start() + + let timer = DispatchSource.makeTimerSource(queue: timerQueue) + self.timer = timer + + timer.schedule(deadline: .now() + Self.measurementInterval, repeating: Self.measurementInterval) + timer.setEventHandler { [weak self] in + guard let self else { return } + + Task { + await self.measureLatency() + } + } + + timer.setCancelHandler { [weak self] in + self?.timer = nil + } + + timer.resume() + } + + private func stopScheduledTimer() async { + await timerRunCoordinator.stop() + + cancelTimerImmediately() + } + + private func cancelTimerImmediately() { + guard let timer else { return } + + if !timer.isCancelled { + timer.cancel() + } + + self.timer = nil + } + + // MARK: - Latency monitor + + @MainActor + public func measureLatency() async { + guard let serverIP = serverIP() else { + latencySubject.send(Self.unknownLatency) + return + } + + os_log("⚫️ Pinging %{public}s", log: .networkProtectionPixel, type: .debug, serverIP.debugDescription) + + let result = await Pinger(ip: serverIP, timeout: Self.pingTimeout, log: .networkProtectionPixel).ping() + + switch result { + case .success(let pingResult): + latencySubject.send(pingResult.time * 1000) + case .failure(let error): + os_log("⚫️ Ping error: %{public}s", log: .networkProtectionPixel, type: .debug, error.localizedDescription) + latencySubject.send(Self.unknownLatency) + } + } + + public func simulateLatency(_ timeInterval: TimeInterval) { + ignoreThreshold = true + latencySubject.send(timeInterval) + ignoreThreshold = false + } +} + +public final class ExponentialGeometricAverage { + private static let decayConstant = 0.1 + private let cutover = ceil(1 / decayConstant) + + private var count = TimeInterval(0) + private var value = TimeInterval(-1) + + public var average: TimeInterval { + value + } + + public func addMeasurement(_ measurement: TimeInterval) { + let keepConstant = 1 - Self.decayConstant + + if count > cutover { + value = exp(keepConstant * log(value) + Self.decayConstant * log(measurement)) + } else if count > 0 { + let retained: Double = keepConstant * count / (count + 1.0) + let newcomer = 1.0 - retained + value = exp(retained * log(value) + newcomer * log(measurement)) + } else { + value = measurement + } + count += 1 + } + + public func reset() { + value = -1.0 + count = 0 + } +} diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyReporter.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyReporter.swift deleted file mode 100644 index d03635640..000000000 --- a/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyReporter.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// NetworkProtectionLatencyReporter.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 Combine -import Foundation -import Common -import Network - -protocol LatencyMeasurer: Sendable { - func ping() async -> Result -} -extension Pinger: LatencyMeasurer {} - -actor NetworkProtectionLatencyReporter { - - struct Configuration { - let firstPingDelay: TimeInterval - let pingInterval: TimeInterval - - let timeout: TimeInterval - let waitForNextConnectionTypeQuery: TimeInterval - - init(firstPingDelay: TimeInterval = .minutes(15), - pingInterval: TimeInterval = .hours(4), - timeout: TimeInterval = .seconds(5), - waitForNextConnectionTypeQuery: TimeInterval = .seconds(15)) { - - self.firstPingDelay = firstPingDelay - self.pingInterval = pingInterval - self.timeout = timeout - self.waitForNextConnectionTypeQuery = waitForNextConnectionTypeQuery - } - - static let `default` = Configuration() - } - - private let configuration: Configuration - private let networkPathMonitor: NWPathMonitor - private var currentConnectionType: NetworkConnectionType? - - private nonisolated let getLogger: (@Sendable () -> OSLog) - - @MainActor - private var task: Task? { - willSet { - task?.cancel() - } - } - - @MainActor - private(set) var currentIP: IPv4Address? - @MainActor - var isStarted: Bool { - task?.isCancelled == false - } - - typealias PingerFactory = @Sendable (IPv4Address, TimeInterval) -> LatencyMeasurer - private let pingerFactory: PingerFactory - - init(configuration: Configuration = .default, - log: @autoclosure @escaping (@Sendable () -> OSLog) = .disabled, - pingerFactory: PingerFactory? = nil) { - - self.configuration = configuration - self.getLogger = log - self.pingerFactory = pingerFactory ?? { ip, timeout in - Pinger(ip: ip, timeout: timeout, log: log()) - } - - let networkPathMonitor = NWPathMonitor() - self.networkPathMonitor = networkPathMonitor - - networkPathMonitor.pathUpdateHandler = { [weak self] path in - guard let connectionType = NetworkConnectionType(nwPath: path) else { return } - Task { [weak self] in - await self?.updateCurrentNetworkConnectionType(connectionType) - } - } - networkPathMonitor.start(queue: .global()) - } - - @MainActor - func start(ip: IPv4Address, reportCallback: @escaping @Sendable (TimeInterval, NetworkConnectionType) -> Void) { - let log = { @Sendable [weak self] in self?.getLogger() ?? .disabled } - let pinger = pingerFactory(ip, configuration.timeout) - self.currentIP = ip - - // run periodic latency measurement with initial delay and following interval - task = Task.periodic(delay: configuration.firstPingDelay, interval: configuration.pingInterval) { [weak self, configuration] in - guard let self else { return } - do { - // poll for current connection type (cellular/wifi/eth) set by NWPathMonitor - let networkPath: NetworkConnectionType = try await { - while true { - if let currentConnectionType = await self.currentConnectionType { - return currentConnectionType - } - try await Task.sleep(interval: configuration.waitForNextConnectionTypeQuery) - } - }() - - // ping the host - let latency = try await pinger.ping().get().time - - // report - reportCallback(latency, networkPath) - - } catch { - os_log("ping failed: %s", log: log(), type: .error, error.localizedDescription) - } - } - } - - @MainActor - func stop() { - task = nil - } - - private func updateCurrentNetworkConnectionType(_ connectionType: NetworkConnectionType) { - self.currentConnectionType = connectionType - } - - deinit { - task?.cancel() - networkPathMonitor.cancel() - } - -} diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionTunnelFailureMonitor.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionTunnelFailureMonitor.swift new file mode 100644 index 000000000..c801cf2e2 --- /dev/null +++ b/Sources/NetworkProtection/Diagnostics/NetworkProtectionTunnelFailureMonitor.swift @@ -0,0 +1,197 @@ +// +// NetworkProtectionTunnelFailureMonitor.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 Network +import NetworkExtension +import Common +import Combine + +final public class NetworkProtectionTunnelFailureMonitor { + public enum Result { + case failureDetected + case failureRecovered + + var threshold: TimeInterval { + switch self { + case .failureDetected: // WG handshakes happen every 2 mins, this means we'd miss 2+ handshakes + return .minutes(5) + case .failureRecovered: + return .minutes(2) // WG handshakes happen every 2 mins + } + } + } + + private static let monitoringInterval: TimeInterval = .seconds(10) + + public var publisher: AnyPublisher { + failureSubject.eraseToAnyPublisher() + } + private let failureSubject = PassthroughSubject() + + private actor TimerRunCoordinator { + private(set) var isRunning = false + + func start() { + isRunning = true + } + + func stop() { + isRunning = false + } + } + + private var timer: DispatchSourceTimer? + private let timerRunCoordinator = TimerRunCoordinator() + private let timerQueue: DispatchQueue + + private let tunnelProvider: PacketTunnelProvider + private let networkMonitor = NWPathMonitor() + + private let log: OSLog + + private let lock = NSLock() + + private var _failureReported = false + private(set) var failureReported: Bool { + get { + lock.lock(); defer { lock.unlock() } + return _failureReported + } + set { + lock.lock() + self._failureReported = newValue + lock.unlock() + } + } + + // MARK: - Init & deinit + + init(tunnelProvider: PacketTunnelProvider, timerQueue: DispatchQueue, log: OSLog) { + self.tunnelProvider = tunnelProvider + self.timerQueue = timerQueue + self.log = log + + os_log("[+] %{public}@", log: .networkProtectionMemoryLog, type: .debug, String(describing: self)) + } + + deinit { + os_log("[-] %{public}@", log: .networkProtectionMemoryLog, type: .debug, String(describing: self)) + + cancelTimerImmediately() + } + + // MARK: - Start/Stop monitoring + + func start() async throws { + guard await !timerRunCoordinator.isRunning else { + os_log("Will not start the tunnel failure monitor as it's already running", log: log) + return + } + + os_log("⚫️ Starting tunnel failure monitor", log: log) + + do { + networkMonitor.start(queue: .global()) + + failureReported = false + try await scheduleTimer() + } catch { + os_log("⚫️ Stopping tunnel failure monitor prematurely", log: log) + throw error + } + } + + func stop() async { + os_log("⚫️ Stopping tunnel failure monitor", log: log) + await stopScheduledTimer() + + networkMonitor.cancel() + } + + // MARK: - Timer scheduling + + private func scheduleTimer() async throws { + await stopScheduledTimer() + + await timerRunCoordinator.start() + + let timer = DispatchSource.makeTimerSource(queue: timerQueue) + self.timer = timer + + timer.schedule(deadline: .now() + Self.monitoringInterval, repeating: Self.monitoringInterval) + timer.setEventHandler { [weak self] in + guard let self else { return } + + Task { + try? await self.monitorHandshakes() + } + } + + timer.setCancelHandler { [weak self] in + self?.timer = nil + } + + timer.resume() + } + + private func stopScheduledTimer() async { + await timerRunCoordinator.stop() + + cancelTimerImmediately() + } + + private func cancelTimerImmediately() { + guard let timer else { return } + + if !timer.isCancelled { + timer.cancel() + } + + self.timer = nil + } + + // MARK: - Handshake monitor + + @MainActor + func monitorHandshakes() async throws { + let mostRecentHandshake = await tunnelProvider.mostRecentHandshake() ?? 0 + + let difference = Date().timeIntervalSince1970 - mostRecentHandshake + os_log("⚫️ Last handshake: %{public}f seconds ago", log: .networkProtectionPixel, type: .debug, difference) + + if difference > Result.failureDetected.threshold, isConnected { + if failureReported { + os_log("⚫️ Tunnel failure already reported", log: .networkProtectionPixel, type: .debug) + } else { + failureSubject.send(.failureDetected) + failureReported = true + } + } else if difference <= Result.failureRecovered.threshold, failureReported { + failureSubject.send(.failureRecovered) + failureReported = false + } + } + + var isConnected: Bool { + let path = networkMonitor.currentPath + let connectionType = NetworkConnectionType(nwPath: path) + + return [.wifi, .eth, .cellular].contains(connectionType) && path.status == .satisfied + } +} diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index d3ed84220..74a0f9c62 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -30,10 +30,18 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { public enum Event { case userBecameActive - case reportLatency(ms: Int, server: String, networkType: NetworkConnectionType) + case reportConnectionAttempt(attempt: ConnectionAttempt) + case reportTunnelFailure(result: NetworkProtectionTunnelFailureMonitor.Result) + case reportLatency(result: NetworkProtectionLatencyMonitor.Result) case rekeyCompleted } + public enum ConnectionAttempt { + case connecting + case success + case failure + } + // MARK: - Error Handling enum TunnelError: LocalizedError { @@ -92,7 +100,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } } - public var connectionStatus: ConnectionStatus = .disconnected { + public var connectionStatus: ConnectionStatus = .default { didSet { guard connectionStatus != oldValue else { return @@ -104,7 +112,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } } - public let connectionStatusPublisher = CurrentValueSubject(.disconnected) + public let connectionStatusPublisher = CurrentValueSubject(.default) public var isKillSwitchEnabled: Bool { guard #available(macOS 11.0, iOS 14.2, *) else { return false } @@ -223,6 +231,12 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { bandwidthAnalyzer.record(rxBytes: rx, txBytes: tx) } + // MARK: - Most recent handshake + + public func mostRecentHandshake() async -> TimeInterval? { + try? await adapter.getMostRecentHandshake() + } + // MARK: - Connection tester private var isConnectionTesterEnabled: Bool = true @@ -235,17 +249,14 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { case .connected: self.tunnelHealth.isHavingConnectivityIssues = false self.updateBandwidthAnalyzerAndRekeyIfExpired() - self.startLatencyReporter() case .reconnected: self.tunnelHealth.isHavingConnectivityIssues = false self.updateBandwidthAnalyzerAndRekeyIfExpired() - self.startLatencyReporter() case .disconnected(let failureCount): self.tunnelHealth.isHavingConnectivityIssues = true self.bandwidthAnalyzer.reset() - self.latencyReporter.stop() if failureCount == 1 { self.notificationsPresenter.showReconnectingNotification() @@ -261,31 +272,18 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } }() - @MainActor - private func startLatencyReporter() { - guard let lastSelectedServerInfo, - let ip = lastSelectedServerInfo.ipv4 else { - assertionFailure("could not get server IPv4 address") - self.latencyReporter.stop() - return - } - if self.latencyReporter.isStarted { - if self.latencyReporter.currentIP == ip { - return - } - self.latencyReporter.stop() - } + public lazy var tunnelFailureMonitor = NetworkProtectionTunnelFailureMonitor(tunnelProvider: self, + timerQueue: timerQueue, + log: .networkProtectionPixel) - self.latencyReporter.start(ip: ip) { [serverName=lastSelectedServerInfo.name, providerEvents] latency, networkType in - providerEvents.fire(.reportLatency(ms: Int(latency * 1000), server: serverName, networkType: networkType)) - } - } + public lazy var latencyMonitor = NetworkProtectionLatencyMonitor(serverIP: { [weak self] in self?.lastSelectedServerInfo?.ipv4 }, + timerQueue: timerQueue, + log: .networkProtectionPixel) private var lastTestFailed = false private let bandwidthAnalyzer = NetworkProtectionConnectionBandwidthAnalyzer() private let tunnelHealth: NetworkProtectionTunnelHealthStore private let controllerErrorStore: NetworkProtectionTunnelErrorStore - private let latencyReporter = NetworkProtectionLatencyReporter(log: .networkProtection) // MARK: - Cancellables @@ -318,11 +316,10 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { super.init() - settings.changePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] change in - self?.handleSettingsChange(change) - }.store(in: &cancellables) + observeSettingChanges() + observeConnectionStatusChanges() + observeTunnelFailures() + observeConnectionQuality() } deinit { @@ -366,6 +363,10 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { try loadAuthToken(from: options) } + open func prepareToConnect(using provider: NETunnelProviderProtocol?) { + // no-op + } + open func loadVendorOptions(from provider: NETunnelProviderProtocol?) throws { let vendorOptions = provider?.providerConfiguration @@ -434,9 +435,67 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { self.includedRoutes = (options?[NetworkProtectionOptionKey.includedRoutes] as? [String])?.compactMap(IPAddressRange.init(from:)) ?? [] } + // MARK: - Observing Changes + + private func observeSettingChanges() { + settings.changePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] change in + self?.handleSettingsChange(change) + }.store(in: &cancellables) + } + + private func observeConnectionStatusChanges() { + connectionStatusPublisher + .removeDuplicates() + .scan((old: ConnectionStatus.default, new: ConnectionStatus.default), { ($0.new, $1) }) + .sink { [weak self] changes in + os_log("⚫️ Connection Status Change: %{public}s -> %{public}s", log: .networkProtectionPixel, type: .debug, changes.old.description, changes.new.description) + + switch changes { + case (_, .connecting), (_, .reasserting): + self?.providerEvents.fire(.reportConnectionAttempt(attempt: .connecting)) + case (_, .connected): + self?.providerEvents.fire(.reportConnectionAttempt(attempt: .success)) + case (.connecting, _), (.reasserting, _): + self?.providerEvents.fire(.reportConnectionAttempt(attempt: .failure)) + default: + break + } + } + .store(in: &cancellables) + } + + private func observeTunnelFailures() { + tunnelFailureMonitor.publisher + .sink { [weak self] result in + self?.providerEvents.fire(.reportTunnelFailure(result: result)) + } + .store(in: &cancellables) + } + + private func observeConnectionQuality() { + latencyMonitor.publisher + .flatMap { [weak self] result in + switch result { + case .error: + self?.providerEvents.fire(.reportLatency(result: .error)) + return Empty().eraseToAnyPublisher() + case .quality(let quality): + return Just(quality).eraseToAnyPublisher() + } + } + .sink { [weak self] quality in + self?.providerEvents.fire(.reportLatency(result: .quality(quality))) + } + .store(in: &cancellables) + } + // MARK: - Tunnel Start open override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { + prepareToConnect(using: tunnelProviderProtocol) + connectionStatus = .connecting os_log("Will load options\n%{public}@", log: .networkProtection, String(describing: options)) @@ -1013,6 +1072,20 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { os_log("🔵 Tunnel interface is %{public}@", log: .networkProtection, type: .info, adapter.interfaceName ?? "unknown") + do { + try await tunnelFailureMonitor.start() + } catch { + os_log("⚫️ Tunnel failure monitor error: %{public}@", log: .networkProtectionPixel, type: .error, String(reflecting: error)) + throw error + } + + do { + try await latencyMonitor.start() + } catch { + os_log("⚫️ Latency monitor error: %{public}@", log: .networkProtectionPixel, type: .error, String(reflecting: error)) + throw error + } + do { // These cases only make sense in the context of a connection that had trouble // and is being fixed, so we want to test the connection immediately. @@ -1028,6 +1101,8 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { public func handleAdapterStopped() async { connectionStatus = .disconnected await self.connectionTester.stop() + await self.tunnelFailureMonitor.stop() + await self.latencyMonitor.stop() } // MARK: - Connection Tester @@ -1067,6 +1142,8 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { os_log("Sleep", log: .networkProtectionSleepLog, type: .info) await connectionTester.stop() + await tunnelFailureMonitor.stop() + await latencyMonitor.stop() } public override func wake() { diff --git a/Sources/NetworkProtection/Status/ConnectionStatus.swift b/Sources/NetworkProtection/Status/ConnectionStatus.swift index 0c22dd848..72fff26e3 100644 --- a/Sources/NetworkProtection/Status/ConnectionStatus.swift +++ b/Sources/NetworkProtection/Status/ConnectionStatus.swift @@ -25,6 +25,25 @@ public enum ConnectionStatus: Codable, Equatable { case connected(connectedDate: Date) case connecting case reasserting + + public static var `default`: ConnectionStatus = .disconnected + + public var description: String { + switch self { + case .connected: + return "connected" + case .disconnected: + return "disconnected" + case .notConfigured: + return "not configured" + case .disconnecting: + return "disconnecting" + case .connecting: + return "connecting" + case .reasserting: + return "reasserting" + } + } } /// This struct represents a status change and holds the new status and a timestamp registering when diff --git a/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift b/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift index f7e8e4397..c25b70578 100644 --- a/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift +++ b/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift @@ -47,6 +47,7 @@ public class WireGuardAdapter { private enum ConfigurationFields: String { case rxBytes = "rx_bytes" case txBytes = "tx_bytes" + case mostRecentHandshake = "last_handshake_time_sec" var configLinePrefix: String { switch self { @@ -54,6 +55,8 @@ public class WireGuardAdapter { return "\(rawValue)=" case .txBytes: return "\(rawValue)=" + case .mostRecentHandshake: + return "\(rawValue)=" } } } @@ -213,6 +216,31 @@ public class WireGuardAdapter { } } + /// Retrieves the number of seconds of the most recent handshake for the previously added peer entry, expressed relative to the Unix epoch. + /// + /// - Throws: ConfigReadingError + /// - Returns: Interval between the most recent handshake and the Unix epoch. + /// + public func getMostRecentHandshake() async throws -> TimeInterval { + try await withCheckedThrowingContinuation { continuation in + getRuntimeConfiguration { configuration in + guard let configuration = configuration else { + continuation.resume(throwing: GetBytesTransmittedError.couldNotObtainAdapterConfiguration) + return + } + + var numberOfSeconds = UInt64(0) + let lines = configuration.components(separatedBy: .newlines) + for line in lines where line.hasPrefix(ConfigurationFields.mostRecentHandshake.configLinePrefix) { + numberOfSeconds = UInt64(line.dropFirst(ConfigurationFields.mostRecentHandshake.configLinePrefix.count)) ?? 0 + break + } + + continuation.resume(returning: TimeInterval(numberOfSeconds)) + } + } + } + /// Returns a runtime configuration from WireGuard. /// - Parameter completionHandler: completion handler. public func getRuntimeConfiguration(completionHandler: @escaping (String?) -> Void) { diff --git a/Tests/NetworkProtectionTests/ExponentialGeometricAverageTests.swift b/Tests/NetworkProtectionTests/ExponentialGeometricAverageTests.swift new file mode 100644 index 000000000..59fa7aae9 --- /dev/null +++ b/Tests/NetworkProtectionTests/ExponentialGeometricAverageTests.swift @@ -0,0 +1,31 @@ +// +// ExponentialGeometricAverageTests.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Foundation +@testable import NetworkProtection + +final class ExponentialGeometricAverageTests: XCTestCase { + func testCalculation() { + let ega = ExponentialGeometricAverage() + for index in 0...20 { + ega.addMeasurement(TimeInterval(index)) + XCTAssertEqual(ega.average, 0) + } + } +} diff --git a/Tests/NetworkProtectionTests/NetworkProtectionLatencyMonitorTests.swift b/Tests/NetworkProtectionTests/NetworkProtectionLatencyMonitorTests.swift new file mode 100644 index 000000000..89f9b305e --- /dev/null +++ b/Tests/NetworkProtectionTests/NetworkProtectionLatencyMonitorTests.swift @@ -0,0 +1,102 @@ +// +// NetworkProtectionLatencyMonitorTests.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Combine +@testable import NetworkProtection + +final class NetworkProtectionLatencyMonitorTests: XCTestCase { + private var monitor: NetworkProtectionLatencyMonitor? + private var cancellable: AnyCancellable? + + override func setUp() async throws { + try await super.setUp() + + monitor = NetworkProtectionLatencyMonitor(serverIP: { nil }, timerQueue: DispatchQueue.main, log: .networkProtectionPixel) + + try await monitor?.start() + } + + override func tearDown() async throws { + await monitor?.stop() + } + + func testInvalidIP() async { + let expectation = XCTestExpectation(description: "Invalid IP reported") + cancellable = monitor?.publisher + .sink { result in + switch result { + case .error: + expectation.fulfill() + case .quality: + break + } + } + await monitor?.measureLatency() + await fulfillment(of: [expectation], timeout: 1) + } + + func testPingFailure() async { + let expectation = XCTestExpectation(description: "Ping failure reported") + cancellable = monitor?.publisher + .sink { result in + switch result { + case .error: + expectation.fulfill() + case .quality: + break + } + } + monitor?.simulateLatency(-1) + await fulfillment(of: [expectation], timeout: 1) + } + + func testConnectionQuality() async { + await testConnectionLatency(0.0, expecting: .excellent) + await testConnectionLatency(0.1, expecting: .excellent) + await testConnectionLatency(20.0, expecting: .good) + await testConnectionLatency(21.0, expecting: .good) + await testConnectionLatency(50.0, expecting: .moderate) + await testConnectionLatency(51.0, expecting: .moderate) + await testConnectionLatency(200.0, expecting: .poor) + await testConnectionLatency(201.0, expecting: .poor) + await testConnectionLatency(300.0, expecting: .terrible) + await testConnectionLatency(301.0, expecting: .terrible) + } + + private func testConnectionLatency(_ timeInterval: TimeInterval, expecting expectedQuality: NetworkProtectionLatencyMonitor.ConnectionQuality) async { + let monitor = NetworkProtectionLatencyMonitor(serverIP: { nil }, timerQueue: DispatchQueue.main, log: .networkProtectionPixel) + + var reportedQuality = NetworkProtectionLatencyMonitor.ConnectionQuality.unknown + cancellable = monitor.publisher + .sink { result in + switch result { + case .quality(let quality): + reportedQuality = quality + case .error: + XCTFail("Unexpected result") + } + } + + try? await monitor.start() + monitor.simulateLatency(timeInterval) + await monitor.stop() + + XCTAssertEqual(expectedQuality, reportedQuality) + } +} From c85592d01647b222748bb751ff9cd980399d926b Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Mon, 11 Dec 2023 15:37:55 +0100 Subject: [PATCH 28/39] Quality metrics for Sync (#597) Task/Issue URL: https://app.asana.com/0/0/1206127893364843/f Description: Implement relevant quality metrics for Sync. --- Package.swift | 1 + .../Bookmarks/BookmarksProvider.swift | 7 ++- .../internal/BookmarksResponseHandler.swift | 20 +++++++-- .../Common/MetricsEvent.swift | 26 +++++++++++ .../Credentials/CredentialsProvider.swift | 8 +++- .../internal/CredentialsResponseHandler.swift | 45 +++++++++++-------- .../Settings/SettingsProvider.swift | 11 ++++- .../EmailProtectionSyncHandler.swift | 11 ++++- .../SettingSyncHandler.swift | 7 ++- .../SettingSyncHandling.swift | 2 +- .../internal/SettingsResponseHandler.swift | 16 +++++-- .../helpers/TestSettingSyncHandler.swift | 2 +- 12 files changed, 122 insertions(+), 34 deletions(-) create mode 100644 Sources/SyncDataProviders/Common/MetricsEvent.swift diff --git a/Package.swift b/Package.swift index df391ce3e..fa4283873 100644 --- a/Package.swift +++ b/Package.swift @@ -193,6 +193,7 @@ let package = Package( dependencies: [ "Bookmarks", "BrowserServicesKit", + "Common", "DDGSync", .product(name: "GRDB", package: "GRDB.swift"), "Persistence", diff --git a/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift b/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift index 5e5bd8342..d63e9f691 100644 --- a/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift +++ b/Sources/SyncDataProviders/Bookmarks/BookmarksProvider.swift @@ -19,6 +19,7 @@ import Foundation import Bookmarks import Combine +import Common import CoreData import DDGSync import Persistence @@ -36,10 +37,12 @@ public final class BookmarksProvider: DataProvider { public init( database: CoreDataDatabase, metadataStore: SyncMetadataStore, + metricsEvents: EventMapping? = nil, syncDidUpdateData: @escaping () -> Void, syncDidFinish: @escaping (FaviconsFetcherInput?) -> Void ) { self.database = database + self.metricsEvents = metricsEvents super.init(feature: .init(name: "bookmarks"), metadataStore: metadataStore, syncDidUpdateData: syncDidUpdateData) self.syncDidFinish = { [weak self] in syncDidFinish(self?.faviconsFetcherInput) @@ -111,7 +114,8 @@ public final class BookmarksProvider: DataProvider { clientTimestamp: clientTimestamp, context: context, crypter: crypter, - deduplicateEntities: isInitial + deduplicateEntities: isInitial, + metricsEvents: metricsEvents ) let idsOfItemsToClearModifiedAt = cleanUpSentItems(sent, receivedUUIDs: Set(responseHandler.receivedByUUID.keys), clientTimestamp: clientTimestamp, in: context) try responseHandler.processReceivedBookmarks() @@ -222,6 +226,7 @@ public final class BookmarksProvider: DataProvider { } private let database: CoreDataDatabase + private let metricsEvents: EventMapping? enum Const { static let maxContextSaveRetries = 5 diff --git a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift index b7bb586e6..1fc79f9e9 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift @@ -18,11 +18,14 @@ // import Bookmarks +import Common import CoreData import DDGSync import Foundation final class BookmarksResponseHandler { + let feature: Feature = .init(name: "bookmarks") + let clientTimestamp: Date? let received: [SyncableBookmarkAdapter] let context: NSManagedObjectContext @@ -43,12 +46,21 @@ final class BookmarksResponseHandler { var idsOfDeletedBookmarks = Set() private let decrypt: (String) throws -> String - - init(received: [Syncable], clientTimestamp: Date? = nil, context: NSManagedObjectContext, crypter: Crypting, deduplicateEntities: Bool) throws { + private let metricsEvents: EventMapping? + + init( + received: [Syncable], + clientTimestamp: Date? = nil, + context: NSManagedObjectContext, + crypter: Crypting, + deduplicateEntities: Bool, + metricsEvents: EventMapping? = nil + ) throws { self.clientTimestamp = clientTimestamp self.received = received.map { SyncableBookmarkAdapter(syncable: $0) } self.context = context self.shouldDeduplicateEntities = deduplicateEntities + self.metricsEvents = metricsEvents let secretKey = try crypter.fetchSecretKey() self.decrypt = { try crypter.base64DecodeAndDecrypt($0, using: secretKey) } @@ -232,7 +244,9 @@ final class BookmarksResponseHandler { } return modifiedAt > clientTimestamp }() - if !isModifiedAfterSyncTimestamp { + if isModifiedAfterSyncTimestamp { + metricsEvents?.fire(.localTimestampResolutionTriggered(feature: feature)) + } else { try updateEntity(existingEntity, with: syncable) } diff --git a/Sources/SyncDataProviders/Common/MetricsEvent.swift b/Sources/SyncDataProviders/Common/MetricsEvent.swift new file mode 100644 index 000000000..262a191bb --- /dev/null +++ b/Sources/SyncDataProviders/Common/MetricsEvent.swift @@ -0,0 +1,26 @@ +// +// MetricsEvent.swift +// DuckDuckGo +// +// 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 DDGSync +import Foundation + +public enum MetricsEvent { + case overrideEmailProtectionSettings + case localTimestampResolutionTriggered(feature: Feature) +} diff --git a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift index 0eaa1b999..0488328db 100644 --- a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift +++ b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift @@ -20,6 +20,7 @@ import Foundation import BrowserServicesKit import Combine +import Common import DDGSync import GRDB import SecureStorage @@ -30,10 +31,12 @@ public final class CredentialsProvider: DataProvider { secureVaultFactory: AutofillVaultFactory = AutofillSecureVaultFactory, secureVaultErrorReporter: SecureVaultErrorReporting, metadataStore: SyncMetadataStore, + metricsEvents: EventMapping? = nil, syncDidUpdateData: @escaping () -> Void ) throws { self.secureVaultFactory = secureVaultFactory self.secureVaultErrorReporter = secureVaultErrorReporter + self.metricsEvents = metricsEvents super.init(feature: .init(name: "credentials"), metadataStore: metadataStore, syncDidUpdateData: syncDidUpdateData) } @@ -138,7 +141,9 @@ public final class CredentialsProvider: DataProvider { secureVault: secureVault, database: database, crypter: crypter, - deduplicateEntities: isInitial) + deduplicateEntities: isInitial, + metricsEvents: self.metricsEvents + ) let idsOfItemsToClearModifiedAt = try self.cleanUpSentItems( sent, @@ -231,6 +236,7 @@ public final class CredentialsProvider: DataProvider { private let secureVaultFactory: AutofillVaultFactory private let secureVaultErrorReporter: SecureVaultErrorReporting + private let metricsEvents: EventMapping? enum Const { static let maxContextSaveRetries = 5 diff --git a/Sources/SyncDataProviders/Credentials/internal/CredentialsResponseHandler.swift b/Sources/SyncDataProviders/Credentials/internal/CredentialsResponseHandler.swift index 42b86ae98..31164c437 100644 --- a/Sources/SyncDataProviders/Credentials/internal/CredentialsResponseHandler.swift +++ b/Sources/SyncDataProviders/Credentials/internal/CredentialsResponseHandler.swift @@ -18,11 +18,14 @@ // import BrowserServicesKit +import Common import DDGSync import Foundation import GRDB final class CredentialsResponseHandler { + let feature: Feature = .init(name: "credentials") + let clientTimestamp: Date let received: [SyncableCredentialsAdapter] let secureVault: any AutofillSecureVault @@ -33,18 +36,23 @@ final class CredentialsResponseHandler { private var credentialsByUUID: [String: SecureVaultModels.SyncableCredentials] = [:] private let decrypt: (String) throws -> String - - init(received: [Syncable], - clientTimestamp: Date, - secureVault: any AutofillSecureVault, - database: Database, - crypter: Crypting, - deduplicateEntities: Bool) throws { + private let metricsEvents: EventMapping? + + init( + received: [Syncable], + clientTimestamp: Date, + secureVault: any AutofillSecureVault, + database: Database, + crypter: Crypting, + deduplicateEntities: Bool, + metricsEvents: EventMapping? = nil + ) throws { self.clientTimestamp = clientTimestamp self.received = received.map(SyncableCredentialsAdapter.init) self.secureVault = secureVault self.database = database self.shouldDeduplicateEntities = deduplicateEntities + self.metricsEvents = metricsEvents let secretKey = try crypter.fetchSecretKey() self.decrypt = { try crypter.base64DecodeAndDecrypt($0, using: secretKey) } @@ -103,17 +111,18 @@ final class CredentialsResponseHandler { } return modifiedAt > clientTimestamp }() - if !isModifiedAfterSyncTimestamp || syncable.isDeleted { - if syncable.isDeleted { - try secureVault.deleteSyncableCredentials(existingEntity, in: database) - } else { - try existingEntity.update(with: syncable, decryptedUsing: decrypt) - existingEntity.metadata.lastModified = nil - try secureVault.storeSyncableCredentials(existingEntity, - in: database, - encryptedUsing: secureVaultEncryptionKey, - hashedUsing: secureVaultHashingSalt) - } + + if syncable.isDeleted { + try secureVault.deleteSyncableCredentials(existingEntity, in: database) + } else if isModifiedAfterSyncTimestamp { + metricsEvents?.fire(.localTimestampResolutionTriggered(feature: feature)) + } else { + try existingEntity.update(with: syncable, decryptedUsing: decrypt) + existingEntity.metadata.lastModified = nil + try secureVault.storeSyncableCredentials(existingEntity, + in: database, + encryptedUsing: secureVaultEncryptionKey, + hashedUsing: secureVaultHashingSalt) } } else if !syncable.isDeleted { diff --git a/Sources/SyncDataProviders/Settings/SettingsProvider.swift b/Sources/SyncDataProviders/Settings/SettingsProvider.swift index 05c3dcb43..549d68549 100644 --- a/Sources/SyncDataProviders/Settings/SettingsProvider.swift +++ b/Sources/SyncDataProviders/Settings/SettingsProvider.swift @@ -20,6 +20,7 @@ import Foundation import BrowserServicesKit import Combine +import Common import CoreData import DDGSync import Persistence @@ -56,6 +57,7 @@ public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { metadataDatabase: CoreDataDatabase, metadataStore: SyncMetadataStore, settingsHandlers: [SettingSyncHandler], + metricsEvents: EventMapping? = nil, syncDidUpdateData: @escaping () -> Void ) { let settingsHandlersBySetting = settingsHandlers.reduce(into: [Setting: any SettingSyncHandling]()) { partialResult, handler in @@ -68,6 +70,7 @@ public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { metadataDatabase: metadataDatabase, metadataStore: metadataStore, settingsHandlersBySetting: settingsHandlers, + metricsEvents: metricsEvents, syncDidUpdateData: syncDidUpdateData ) @@ -82,10 +85,12 @@ public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { metadataDatabase: CoreDataDatabase, metadataStore: SyncMetadataStore, settingsHandlersBySetting: [Setting: any SettingSyncHandling], + metricsEvents: EventMapping? = nil, syncDidUpdateData: @escaping () -> Void ) { self.metadataDatabase = metadataDatabase self.settingsHandlers = settingsHandlersBySetting + self.metricsEvents = metricsEvents super.init(feature: .init(name: "settings"), metadataStore: metadataStore, syncDidUpdateData: syncDidUpdateData) } @@ -218,7 +223,8 @@ public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { settingsHandlers: settingsHandlers, context: context, crypter: crypter, - deduplicateEntities: isInitial + deduplicateEntities: isInitial, + metricsEvents: metricsEvents ) let idsOfItemsToClearModifiedAt = try cleanUpSentItems( sent, @@ -294,7 +300,7 @@ public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { let hasNewerVersionOnServer: Bool = receivedKeys.contains(metadata.key) let isPendingDeletion = originalValues[setting] == nil if isPendingDeletion, !hasNewerVersionOnServer { - try handler.setValue(nil) + try handler.setValue(nil, shouldDetectOverride: false) } else { idsOfItemsToClearModifiedAt.insert(metadata.key) } @@ -343,6 +349,7 @@ public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { private let metadataDatabase: CoreDataDatabase private let settingsHandlers: [Setting: any SettingSyncHandling] private let errorSubject = PassthroughSubject() + private let metricsEvents: EventMapping? enum Const { static let maxContextSaveRetries = 5 diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift index 861ffc3d1..d1bb4181b 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift @@ -59,13 +59,22 @@ public final class EmailProtectionSyncHandler: SettingSyncHandler { return String(bytes: data, encoding: .utf8) } - public override func setValue(_ value: String?) throws { + public override func setValue(_ value: String?, shouldDetectOverride: Bool) throws { + guard let value, let valueData = value.data(using: .utf8) else { + if shouldDetectOverride, try emailManager.getUsername() != nil { + metricsEvents?.fire(.overrideEmailProtectionSettings) + } try emailManager.signOut(isForced: false) return } let payload = try JSONDecoder.snakeCaseKeys.decode(Payload.self, from: valueData) + + if shouldDetectOverride, let username = try emailManager.getUsername(), payload.username != username { + metricsEvents?.fire(.overrideEmailProtectionSettings) + } + try emailManager.signIn(username: payload.username, token: payload.personalAccessToken) } diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandler.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandler.swift index 643699ec9..5331d2767 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandler.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandler.swift @@ -18,6 +18,7 @@ // import Combine +import Common import Foundation open class SettingSyncHandler: SettingSyncHandling { @@ -37,11 +38,12 @@ open class SettingSyncHandler: SettingSyncHandling { return nil } - open func setValue(_ value: String?) throws { + open func setValue(_ value: String?, shouldDetectOverride: Bool) throws { assertionFailure("implementation missing for \(#function)") } - public init() { + public init(metricsEvents: EventMapping? = nil) { + self.metricsEvents = metricsEvents valueDidChangeCancellable = valueDidChangePublisher .sink { [weak self] in guard let self else { @@ -52,6 +54,7 @@ open class SettingSyncHandler: SettingSyncHandling { } } + let metricsEvents: EventMapping? weak var delegate: SettingSyncHandlingDelegate? private var valueDidChangeCancellable: AnyCancellable? } diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandling.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandling.swift index 5f4dbc8b0..0d9948d6b 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandling.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandling.swift @@ -54,7 +54,7 @@ protocol SettingSyncHandling: AnyObject { * * Note: If "delete" was received from Sync, `value` is `nil`. */ - func setValue(_ value: String?) throws + func setValue(_ value: String?, shouldDetectOverride: Bool) throws /** * Delegate that must be notified about updating setting's value. diff --git a/Sources/SyncDataProviders/Settings/internal/SettingsResponseHandler.swift b/Sources/SyncDataProviders/Settings/internal/SettingsResponseHandler.swift index 7576a690b..18dd57798 100644 --- a/Sources/SyncDataProviders/Settings/internal/SettingsResponseHandler.swift +++ b/Sources/SyncDataProviders/Settings/internal/SettingsResponseHandler.swift @@ -18,11 +18,14 @@ // import Bookmarks +import Common import CoreData import DDGSync import Foundation final class SettingsResponseHandler { + let feature: Feature = .init(name: "settings") + let clientTimestamp: Date? let received: [SyncableSettingAdapter] let context: NSManagedObjectContext @@ -34,6 +37,7 @@ final class SettingsResponseHandler { var idsOfItemsThatRetainModifiedAt = Set() private let decrypt: (String) throws -> String + private let metricsEvents: EventMapping? init( received: [Syncable], @@ -41,7 +45,8 @@ final class SettingsResponseHandler { settingsHandlers: [SettingsProvider.Setting: any SettingSyncHandling], context: NSManagedObjectContext, crypter: Crypting, - deduplicateEntities: Bool + deduplicateEntities: Bool, + metricsEvents: EventMapping? = nil ) throws { self.clientTimestamp = clientTimestamp @@ -49,6 +54,7 @@ final class SettingsResponseHandler { self.settingsHandlers = settingsHandlers self.context = context self.shouldDeduplicateEntities = deduplicateEntities + self.metricsEvents = metricsEvents let secretKey = try crypter.fetchSecretKey() self.decrypt = { try crypter.base64DecodeAndDecrypt($0, using: secretKey) } @@ -86,10 +92,10 @@ final class SettingsResponseHandler { private func update(_ setting: SettingsProvider.Setting, with syncable: SyncableSettingAdapter) throws { if syncable.isDeleted { - try settingsHandlers[setting]?.setValue(nil) + try settingsHandlers[setting]?.setValue(nil, shouldDetectOverride: shouldDeduplicateEntities) } else { let value = try syncable.encryptedValue.flatMap { try decrypt($0) } - try settingsHandlers[setting]?.setValue(value) + try settingsHandlers[setting]?.setValue(value, shouldDetectOverride: shouldDeduplicateEntities) } if let metadata = metadataByKey[setting.key] { metadata.lastModified = nil @@ -111,7 +117,9 @@ final class SettingsResponseHandler { } return lastModified > clientTimestamp }() - if !isModifiedAfterSyncTimestamp { + if isModifiedAfterSyncTimestamp { + metricsEvents?.fire(.localTimestampResolutionTriggered(feature: feature)) + } else { try update(setting, with: syncable) } } else { diff --git a/Tests/SyncDataProvidersTests/Settings/helpers/TestSettingSyncHandler.swift b/Tests/SyncDataProvidersTests/Settings/helpers/TestSettingSyncHandler.swift index f7448e249..bc3457fb5 100644 --- a/Tests/SyncDataProvidersTests/Settings/helpers/TestSettingSyncHandler.swift +++ b/Tests/SyncDataProvidersTests/Settings/helpers/TestSettingSyncHandler.swift @@ -36,7 +36,7 @@ final class TestSettingSyncHandler: SettingSyncHandler { syncedValue } - override func setValue(_ value: String?) throws { + override func setValue(_ value: String?, shouldDetectOverride: Bool) throws { DispatchQueue.main.async { self.notifyValueDidChange = false self.syncedValue = value From e4f4ae624174c1398d345cfc387db38f8f69986d Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Mon, 11 Dec 2023 12:50:28 -0800 Subject: [PATCH 29/39] Fix an IPv6 regression. (#598) Required: Task/Issue URL: https://app.asana.com/0/0/1206136376883995/f iOS PR: duckduckgo/iOS#2258 macOS PR: duckduckgo/macos-browser#1954 What kind of version bump will this require?: Major Description: This PR fixes a regression related to IPv6. --- .../PacketTunnelProvider.swift | 2 +- .../WireGuardKit/Endpoint.swift | 15 +++- .../EndpointTests.swift | 73 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 Tests/NetworkProtectionTests/EndpointTests.swift diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 74a0f9c62..5748e1587 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -989,7 +989,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } private func handleGetServerAddress(completionHandler: ((Data?) -> Void)? = nil) { - let response = lastSelectedServerInfo?.endpoint.map { ExtensionMessageString($0.host.description) } + let response = lastSelectedServerInfo?.endpoint.map { ExtensionMessageString($0.host.hostWithoutPort) } completionHandler?(response?.rawValue) } diff --git a/Sources/NetworkProtection/WireGuardKit/Endpoint.swift b/Sources/NetworkProtection/WireGuardKit/Endpoint.swift index 9d9502dd3..f4259e8f8 100644 --- a/Sources/NetworkProtection/WireGuardKit/Endpoint.swift +++ b/Sources/NetworkProtection/WireGuardKit/Endpoint.swift @@ -30,7 +30,16 @@ extension Endpoint: Hashable { extension Endpoint: CustomStringConvertible { public var description: String { - "\(host):\(port)" + switch host { + case .name(let hostname, _): + return "\(hostname):\(port)" + case .ipv4(let address): + return "\(address):\(port)" + case .ipv6(let address): + return "[\(address)]:\(port)" + @unknown default: + fatalError() + } } public init?(from string: String) { @@ -80,8 +89,8 @@ extension Endpoint { } } -extension NWEndpoint.Host: CustomStringConvertible { - public var description: String { +extension NWEndpoint.Host { + public var hostWithoutPort: String { switch self { case .name(let hostname, _): return hostname diff --git a/Tests/NetworkProtectionTests/EndpointTests.swift b/Tests/NetworkProtectionTests/EndpointTests.swift new file mode 100644 index 000000000..b732948c8 --- /dev/null +++ b/Tests/NetworkProtectionTests/EndpointTests.swift @@ -0,0 +1,73 @@ +// +// File.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import Network +@testable import NetworkProtection + +final class EndpointTests: XCTestCase { + + func testEndpointWithHostName_ShouldIncludePort() { + let endpoint = self.endpointWithHostName() + XCTAssertEqual(endpoint.description, "https://duckduckgo.com:443") + } + + func testEndpointWithIPv4Address_ShouldIncludePort() { + let endpoint = self.endpointWithIPv4() + XCTAssertEqual(endpoint.description, "52.250.42.157:443") + } + + func testEndpointWithIPv6Address_ShouldIncludePort() { + let endpoint = self.endpointWithIPv6() + XCTAssertEqual(endpoint.description, "[2001:db8:85a3::8a2e:370:7334]:443") + } + + func testParsingEndpointFromIPv4Address() { + let address = "52.250.42.157:443" + let endpoint = Endpoint(from: address)! + XCTAssertEqual(endpoint.description, address) + } + + func testParsingEndpointFromIPv6Address() { + let address = "[2001:0db8:85a3:0000:0000:8a2e:0370]:443" + let endpoint = Endpoint(from: address)! + XCTAssertEqual(endpoint.description, "2001:0db8:85a3:0000:0000:8a2e:0370:443") + } + + private func endpointWithHostName() -> Endpoint { + let host = NWEndpoint.Host.name("https://duckduckgo.com", nil) + let port = NWEndpoint.Port.https + return Endpoint(host: host, port: port) + } + + private func endpointWithIPv4() -> Endpoint { + let address = IPv4Address("52.250.42.157")! + let host = NWEndpoint.Host.ipv4(address) + let port = NWEndpoint.Port.https + return Endpoint(host: host, port: port) + } + + private func endpointWithIPv6() -> Endpoint { + let address = IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334")! + let host = NWEndpoint.Host.ipv6(address) + let port = NWEndpoint.Port.https + return Endpoint(host: host, port: port) + } + +} From 7b0910360d6f700ca9bea5e5374c8e2c9a2da899 Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Fri, 15 Dec 2023 14:24:06 +0100 Subject: [PATCH 30/39] Remove the reconnect/disconnect logic from the connection tester Task/Issue URL: https://app.asana.com/0/0/1206173513538620/f iOS PR: https://github.com/duckduckgo/iOS/pull/2272 macOS PR: https://github.com/duckduckgo/macos-browser/pull/1970 What kind of version bump will this require?: Patch ## Description Removes the logic that would reconnect / disconnect NetP in case of trouble. --- .../NetworkProtectionConnectionTester.swift | 30 ++++++-------- .../PacketTunnelProvider.swift | 41 +------------------ .../DisabledConnectivityIssueObserver.swift | 32 +++++++++++++++ 3 files changed, 46 insertions(+), 57 deletions(-) create mode 100644 Sources/NetworkProtection/Status/ConnectivityIssueObserver/DisabledConnectivityIssueObserver.swift diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionConnectionTester.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionConnectionTester.swift index 2a059b5b6..c765177e8 100644 --- a/Sources/NetworkProtection/Diagnostics/NetworkProtectionConnectionTester.swift +++ b/Sources/NetworkProtection/Diagnostics/NetworkProtectionConnectionTester.swift @@ -94,13 +94,13 @@ final class NetworkProtectionConnectionTester { // MARK: - Test result handling private var failureCount = 0 - private let resultHandler: @MainActor (Result, Bool) -> Void + private let resultHandler: @MainActor (Result) -> Void private var simulateFailure = false // MARK: - Init & deinit - init(timerQueue: DispatchQueue, log: OSLog, resultHandler: @escaping @MainActor (Result, Bool) -> Void) { + init(timerQueue: DispatchQueue, log: OSLog, resultHandler: @escaping @MainActor (Result) -> Void) { self.timerQueue = timerQueue self.log = log self.resultHandler = resultHandler @@ -182,7 +182,7 @@ final class NetworkProtectionConnectionTester { if testImmediately { do { - try await testConnection(isStartupTest: true) + try await testConnection() } catch { os_log("Rethrowing exception", log: log) throw error @@ -194,17 +194,13 @@ final class NetworkProtectionConnectionTester { timer.schedule(deadline: .now() + self.intervalBetweenTests, repeating: self.intervalBetweenTests) timer.setEventHandler { [weak self] in - guard let testConnection = self?.testConnection(isStartupTest:) else { - return - } - - Task { + Task { [self] in // During regular connection tests we don't care about the error thrown // by this method, as it'll be handled through the result handler callback. // The error we're ignoring here is only used when this class is initialized // with an immediate test, to know whether the connection is up while the user // still sees "Connecting..." - try? await testConnection(false) + try? await self?.testConnection() } } @@ -235,7 +231,7 @@ final class NetworkProtectionConnectionTester { // MARK: - Testing the connection - func testConnection(isStartupTest: Bool) async throws { + func testConnection() async throws { guard let tunnelInterface = tunnelInterface else { os_log("No interface to test!", log: log, type: .error) return @@ -267,11 +263,11 @@ final class NetworkProtectionConnectionTester { if onlyVPNIsDown { os_log("👎 VPN is DOWN", log: log) - await handleDisconnected(isStartupTest: isStartupTest) + await handleDisconnected() } else { os_log("👍 VPN: \(vpnIsConnected ? "UP" : "DOWN") local: \(localIsConnected ? "UP" : "DOWN")", log: log) - await handleConnected(isStartupTest: isStartupTest) + await handleConnected() } } @@ -303,19 +299,19 @@ final class NetworkProtectionConnectionTester { // MARK: - Result handling @MainActor - private func handleConnected(isStartupTest: Bool) { + private func handleConnected() { if failureCount == 0 { - resultHandler(.connected, isStartupTest) + resultHandler(.connected) } else if failureCount > 0 { failureCount = 0 - resultHandler(.reconnected, isStartupTest) + resultHandler(.reconnected) } } @MainActor - private func handleDisconnected(isStartupTest: Bool) { + private func handleDisconnected() { failureCount += 1 - resultHandler(.disconnected(failureCount: failureCount), isStartupTest) + resultHandler(.disconnected(failureCount: failureCount)) } } diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index 5748e1587..f2634d486 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -47,7 +47,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { enum TunnelError: LocalizedError { case startingTunnelWithoutAuthToken case couldNotGenerateTunnelConfiguration(internalError: Error) - case couldNotFixConnection case simulateTunnelFailureError var errorDescription: String? { @@ -58,11 +57,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { return "Failed to generate a tunnel configuration: \(internalError.localizedDescription)" case .simulateTunnelFailureError: return "Simulated a tunnel error as requested" - default: - // This is probably not the most elegant error to show to a user but - // it's a great way to get detailed reports for those cases we haven't - // provided good descriptions for yet. - return "Tunnel error: \(String(describing: self))" } } } @@ -242,7 +236,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { private var isConnectionTesterEnabled: Bool = true private lazy var connectionTester: NetworkProtectionConnectionTester = { - NetworkProtectionConnectionTester(timerQueue: timerQueue, log: .networkProtectionConnectionTesterLog) { @MainActor [weak self] (result, isStartupTest) in + NetworkProtectionConnectionTester(timerQueue: timerQueue, log: .networkProtectionConnectionTesterLog) { @MainActor [weak self] result in guard let self else { return } switch result { @@ -257,17 +251,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { case .disconnected(let failureCount): self.tunnelHealth.isHavingConnectivityIssues = true self.bandwidthAnalyzer.reset() - - if failureCount == 1 { - self.notificationsPresenter.showReconnectingNotification() - - // Only do these things if this is not a connection startup test. - if !isStartupTest { - self.fixTunnel() - } - } else if failureCount == 2 { - self.stopTunnel(with: TunnelError.couldNotFixConnection) - } } } }() @@ -686,28 +669,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { controllerErrorStore.lastErrorMessage = nil } - /// Intentionally not async, so that we won't lock whoever called this method. This method will race against the tester - /// to see if it can fix the connection before the next failure. - /// - private func fixTunnel() { - Task { - let serverSelectionMethod: NetworkProtectionServerSelectionMethod - - if let lastServerName = lastSelectedServerInfo?.name { - serverSelectionMethod = .avoidServer(serverName: lastServerName) - } else { - assertionFailure("We should not have a situation where the VPN is trying to fix the tunnel and there's no previous server info") - serverSelectionMethod = .automatic - } - - do { - try await updateTunnelConfiguration(environment: settings.selectedEnvironment, serverSelectionMethod: serverSelectionMethod) - } catch { - return - } - } - } - // MARK: - Tunnel Configuration @MainActor diff --git a/Sources/NetworkProtection/Status/ConnectivityIssueObserver/DisabledConnectivityIssueObserver.swift b/Sources/NetworkProtection/Status/ConnectivityIssueObserver/DisabledConnectivityIssueObserver.swift new file mode 100644 index 000000000..a263b3558 --- /dev/null +++ b/Sources/NetworkProtection/Status/ConnectivityIssueObserver/DisabledConnectivityIssueObserver.swift @@ -0,0 +1,32 @@ +// +// DisabledConnectivityIssueObserver.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 Combine +import Foundation + +/// This is a convenience temporary disabler for the connectivity issues observer. Will always report no issues. +/// +/// This is useful since we decided to momentarily disable connection issue reporting in the UI: +/// ref: https://app.asana.com/0/0/1206071632962016/1206144227620065/f +/// +public final class DisabledConnectivityIssueObserver: ConnectivityIssueObserver { + public var publisher: AnyPublisher = Just(false).eraseToAnyPublisher() + public var recentValue = false + + public init() {} +} From 861b8a72930f138cd18b6a7722502a8a40375827 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:02:48 +0000 Subject: [PATCH 31/39] Update autofill to 10.0.2 (#599) Task/Issue URL: https://app.asana.com/0/1206157971479455/1206157971479455 Autofill Release: https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/10.0.2 Description Updates Autofill to version 10.0.2. --- Package.resolved | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8482cdadb..c65a105fd 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", "state" : { - "revision" : "dbecae0df07650a21b5632a92fa2e498c96af7b5", - "version" : "10.0.1" + "revision" : "5597bc17709c8acf454ecaad4f4082007986242a", + "version" : "10.0.2" } }, { diff --git a/Package.swift b/Package.swift index fa4283873..6a4b2d300 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "SecureStorage", targets: ["SecureStorage"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "10.0.1"), + .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "10.0.2"), .package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.2.0"), .package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "1.2.2"), .package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"), From d07e18b3785d31c15364e56bdd2cb4b27edd35d0 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 18 Dec 2023 13:22:59 +0600 Subject: [PATCH 32/39] SwiftLint plugin (#393) Task/Issue URL: https://app.asana.com/0/276630244458377/1204377796839424/f --- .github/workflows/pr.yml | 2 +- .swiftlint.yml | 80 ++++++- Package.swift | 194 +++++++++++---- Plugins/SwiftLintPlugin/InputListItem.swift | 36 +++ Plugins/SwiftLintPlugin/PathExtension.swift | 73 ++++++ .../ProcessInfo+EnvironmentType.swift | 39 +++ Plugins/SwiftLintPlugin/SwiftLintPlugin.swift | 222 ++++++++++++++++++ .../Bookmarks/BookmarkEditorViewModel.swift | 10 +- Sources/Bookmarks/BookmarkEntity.swift | 29 ++- Sources/Bookmarks/BookmarkErrors.swift | 14 +- Sources/Bookmarks/BookmarkListViewModel.swift | 16 +- Sources/Bookmarks/BookmarkUtils.swift | 8 +- .../Bookmarks/BookmarksDatabaseCleaner.swift | 3 +- Sources/Bookmarks/BookmarksModel.swift | 6 +- .../BookmarksFaviconsFetcher.swift | 1 - .../BookmarksFaviconsFetcherStateStore.swift | 1 - .../FaviconsFetcher/FaviconFetcher.swift | 1 - .../FaviconsFetchOperation.swift | 1 - Sources/Bookmarks/FavoriteListViewModel.swift | 26 +- Sources/Bookmarks/FavoritesDisplayMode.swift | 1 - .../BookmarkCoreDataImporter.swift | 32 +-- .../ImportExport/BookmarkOrFolder.swift | 1 - .../Bookmarks/MenuBookmarksViewModel.swift | 28 +-- ...BookmarkFormFactorFavoritesMigration.swift | 6 +- .../BookmarksTestDBBuilder.swift | 1 - .../BookmarksTestsUtils/BookmarkTree.swift | 3 - .../ModelAccessHelper.swift | 1 - .../Autofill/AutofillUserScript+Email.swift | 3 +- .../AutofillUserScript+SecureVault.swift | 59 ++--- .../AutofillUserScript+SourceProvider.swift | 1 - .../Autofill/AutofillUserScript.swift | 9 +- .../Matchers/AutofillAccountMatcher.swift | 1 - .../Matchers/AutofillUrlMatcher.swift | 1 - .../Autofill/OverlayAutofillUserScript.swift | 1 - .../Autofill/Sort/AutofillUrlSort.swift | 1 - .../Autofill/WebsiteAutofillUserScript.swift | 5 +- .../AdClickAttributionCounter.swift | 11 +- .../AdClickAttributionCounterStore.swift | 3 +- .../AdClickAttributionDetection.swift | 53 ++--- .../AdClickAttributionEvents.swift | 11 +- .../AdClickAttributionLogic.swift | 58 +++-- .../AdClickAttributionRulesMutator.swift | 21 +- .../AdClickAttributionRulesProvider.swift | 65 +++-- .../AdClickAttributionRulesSplitter.swift | 37 ++- .../ContentBlockerDebugEvents.swift | 11 +- .../ContentBlockerRulesIdentifier.swift | 35 ++- .../ContentBlockerRulesManager.swift | 31 +-- .../ContentBlockerRulesSource.swift | 7 +- .../ContentBlockerRulesSourceManager.swift | 9 +- .../ContentBlockingRulesCompilationTask.swift | 1 - ...kingRulesLastCompiledRulesLookupTask.swift | 7 +- .../ContentBlockingRulesLookupTask.swift | 1 - .../DomainsProtectionStore.swift | 1 - .../FailedCompilationsStore.swift | 1 - .../LastCompiledRulesStore.swift | 9 +- .../ContentBlocking/TrackerDataManager.swift | 21 +- .../TrackerDataQueryExtension.swift | 17 +- .../ContentBlockerRulesUserScript.swift | 41 ++-- .../UserScripts/SurrogatesUserScript.swift | 5 +- .../UserScripts/TrackerResolver.swift | 37 ++- .../ContentScopeUserScript.swift | 1 - .../SpecialPagesUserScript.swift | 1 - .../Email/EmailKeychainManager.swift | 51 ++-- .../Email/EmailManager.swift | 55 ++--- .../FeatureFlagger/FeatureFlagger.swift | 1 - .../GPC/GPCRequestFactory.swift | 17 +- .../InternalUserDecider.swift | 13 +- .../AMPCanonicalExtractor.swift | 47 ++-- .../AMPProtectionDebugEvents.swift | 5 +- .../LinkProtection/LinkCleaner.swift | 37 ++- .../LinkProtection/LinkProtection.swift | 28 +-- .../LinkProtection/TrackingLinkSettings.swift | 11 +- .../AppPrivacyConfiguration.swift | 39 ++- .../PrivacyConfig/AppVersionProvider.swift | 3 +- .../Features/AdClickAttributionFeature.swift | 49 ++-- .../Features/PrivacyFeature.swift | 1 - .../PrivacyConfig/PrivacyConfiguration.swift | 1 - .../PrivacyConfigurationData.swift | 7 +- .../PrivacyConfigurationManager.swift | 24 +- .../ReferrerTrimming/ReferrerTrimming.swift | 46 ++-- .../AutofillDatabaseProvider.swift | 55 +++-- .../AutofillKeyStoreProvider.swift | 10 +- .../SecureVault/AutofillSecureVault.swift | 26 +- .../CredentialsDatabaseCleaner.swift | 1 - .../SecureVault/SecureVaultManager.swift | 77 +++--- .../SecureVault/SecureVaultModels+Sync.swift | 1 - .../SecureVault/SecureVaultModels.swift | 18 +- .../EmbeddedBloomFilterResources.swift | 1 - .../HTTPSBloomFilterSpecification.swift | 4 +- .../HTTPSExcludedDomains.swift | 5 +- .../SmarterEncryption/HTTPSUpgrade.swift | 3 +- .../HTTPSUpgradeParser.swift | 5 +- .../SmarterEncryption/HTTPSUpgradeStore.swift | 2 +- .../Store/AppHTTPSUpgradeStore.swift | 1 - .../Store/HTTPSExcludedDomain.swift | 3 +- .../HTTPSStoredBloomFilterSpecification.swift | 1 - .../HTTPSUpgradeManagedObjectModel.swift | 1 - .../Statistics/StatisticsStore.swift | 1 - .../Statistics/VariantManager.swift | 7 +- .../Suggestions/Suggestion.swift | 2 +- .../Suggestions/SuggestionProcessing.swift | 4 +- .../Suggestions/SuggestionResult.swift | 2 +- Sources/Common/AppVersion.swift | 11 +- Sources/Common/Combine/ScheduledFuture.swift | 2 +- Sources/Common/Debug.swift | 2 - Sources/Common/DecodableHelper.swift | 1 - Sources/Common/EventMapping.swift | 1 - .../Common/Extensions/ArrayExtension.swift | 1 - .../Common/Extensions/BundleExtension.swift | 3 +- .../Common/Extensions/CalendarExtension.swift | 3 +- .../Extensions/FileManagerExtension.swift | 5 +- Sources/Common/Extensions/HashExtension.swift | 13 +- .../Common/Extensions/JSONExtensions.swift | 1 - .../Common/Extensions/NSObjectExtension.swift | 1 - .../Common/Extensions/RunLoopExtension.swift | 1 - .../Common/Extensions/StringExtension.swift | 6 +- Sources/Common/Extensions/URLExtension.swift | 27 +-- .../UnsafeMutableRawPointerExtension.swift | 1 - Sources/Common/InfoBundle.swift | 5 +- Sources/Common/JsonError.swift | 1 - Sources/Common/Logging.swift | 3 +- Sources/Common/TLD/TLD.swift | 13 +- Sources/Configuration/Configuration.swift | 13 +- .../Configuration/ConfigurationFetching.swift | 13 +- .../Configuration/ConfigurationStoring.swift | 7 +- .../ConfigurationValidating.swift | 6 +- Sources/ContentBlocking/DetectedRequest.swift | 15 +- Sources/Crashes/CrashCollection.swift | 2 +- Sources/DDGSync/DDGSyncing.swift | 2 +- Sources/DDGSync/DataProvider.swift | 1 - Sources/DDGSync/RecoveryPDFGenerator.swift | 2 +- Sources/DDGSync/SyncFeatureEntity.swift | 1 - Sources/DDGSync/SyncModels.swift | 3 +- .../DDGSync/SyncableSettingsMetadata.swift | 1 - Sources/DDGSync/internal/BookmarkUpdate.swift | 8 +- Sources/DDGSync/internal/Crypter.swift | 4 +- Sources/DDGSync/internal/Endpoints.swift | 2 +- Sources/DDGSync/internal/KeyValueStore.swift | 2 +- .../internal/ProductionDependencies.swift | 4 +- .../internal/RecoveryKeyTransmitter.swift | 1 - .../RemoteAPIRequestCreatingExtensions.swift | 3 +- .../internal/RemoteAPIRequestCreator.swift | 2 +- .../DDGSync/internal/RemoteConnector.swift | 6 +- Sources/DDGSync/internal/SecureStorage.swift | 12 +- Sources/DDGSync/internal/SyncOperation.swift | 3 +- Sources/DDGSync/internal/SyncQueue.swift | 7 +- .../DDGSync/internal/SyncRequestMaker.swift | 3 +- Sources/DDGSync/internal/SyncScheduler.swift | 3 +- .../Navigation/AuthChallengeDisposition.swift | 2 +- .../DistributedNavigationDelegate.swift | 7 +- .../Extensions/WKErrorExtension.swift | 18 +- .../Extensions/WKFrameInfoExtension.swift | 2 +- .../WKNavigationResponseExtension.swift | 2 +- Sources/Navigation/NavigationResponse.swift | 2 +- Sources/Navigation/NavigationType.swift | 2 +- .../Controllers/TunnelController.swift | 4 +- .../Diagnostics/NetworkProtectionError.swift | 1 - .../NetworkProtectionLatencyMonitor.swift | 6 +- ...or+NetworkProtectionErrorConvertible.swift | 3 +- .../ExtensionMessage/ExtensionMessage.swift | 2 +- .../KeyManagement/KeyPair.swift | 2 +- .../NetworkProtectionKeychainStore.swift | 2 +- .../Models/AnyIPAddress.swift | 2 +- .../Models/NetworkProtectionLocation.swift | 1 - .../Networking/NWConnectionExtension.swift | 2 +- .../NetworkProtection/Networking/Pinger.swift | 8 +- .../NetworkProtectionNotification.swift | 2 +- .../PacketTunnelProvider.swift | 13 +- ...workProtectionLocationListRepository.swift | 4 +- .../UserDefaults+vpnFirstEnabled.swift | 2 +- .../Settings/VPNSettings.swift | 4 - ...onnectionErrorObserverThroughSession.swift | 2 +- .../ConnectionServerInfoObserver.swift | 2 +- ...tionServerInfoObserverThroughSession.swift | 2 +- ...ficationsPresenterTogglableDecorator.swift | 1 - ...NetworkProtectionSelectedServerStore.swift | 4 +- .../NetworkProtectionServerListStore.swift | 2 +- ...workProtectionSimulationOptionsStore.swift | 2 +- .../NetworkProtectionTunnelHealthStore.swift | 2 +- .../WireGuardKit/WireGuardAdapter.swift | 9 +- .../Controllers/MockTunnelController.swift | 1 - ...eRedemptionCoordinatorTestExtensions.swift | 3 +- .../MockNetworkProtectionTokenStore.swift | 5 +- .../MockNetworkProtectionClient.swift | 3 +- .../MockNetworkProtectionStatusReporter.swift | 4 +- Sources/Networking/APIHeaders.swift | 13 +- Sources/Networking/APIRequest.swift | 17 +- .../Networking/APIRequestConfiguration.swift | 13 +- Sources/Networking/APIRequestError.swift | 9 +- .../Networking/APIResponseRequirements.swift | 11 +- .../Networking/Extensions/HTTPConstants.swift | 15 +- .../Extensions/HTTPURLResponseExtension.swift | 19 +- .../Extensions/URLRequestAttribution.swift | 7 +- .../Extensions/URLResponseExtension.swift | 5 +- .../Extensions/URLSessionExtension.swift | 7 +- Sources/Persistence/CoreDataDatabase.swift | 38 +-- .../Persistence/CoreDataErrorsParser.swift | 24 +- Sources/Persistence/KeyValueStoring.swift | 3 +- .../Model/AllowedPermission.swift | 2 +- .../Model/CookieConsentInfo.swift | 2 +- .../Model/ProtectionStatus.swift | 5 +- .../PrivacyDashboard/Model/TrackerInfo.swift | 28 +-- .../PrivacyDashboardController.swift | 114 ++++----- Sources/PrivacyDashboard/PrivacyInfo.swift | 14 +- .../PrivacyDashboardUserScript.swift | 100 ++++---- .../ViewModel/ServerTrustViewModel.swift | 52 ++-- .../JsonToRemoteConfigModelMapper.swift | 1 - .../JsonToRemoteMessageModelMapper.swift | 1 - .../Matchers/AppAttributeMatcher.swift | 3 +- .../Matchers/AttributeMatcher.swift | 1 - .../Matchers/DeviceAttributeMatcher.swift | 1 - .../Matchers/EvaluationResult.swift | 1 - .../Matchers/UserAttributeMatcher.swift | 1 - .../RemoteMessaging/Model/AnyDecodable.swift | 6 +- .../Model/JsonRemoteMessagingConfig.swift | 2 +- .../Model/MatchingAttributes.swift | 3 - .../Model/RemoteConfigModel.swift | 1 - .../Model/RemoteMessageModel.swift | 3 +- .../Model/RemoteMessagingConfig.swift | 1 - .../RemoteMessagingConfigMatcher.swift | 1 - .../RemoteMessagingConfigProcessor.swift | 1 - .../SecureStorageCryptoProvider.swift | 2 +- .../SecureStorage/SecureStorageError.swift | 2 +- .../SecureStorage/SecureVaultFactory.swift | 8 +- .../MockCryptoProvider.swift | 1 - .../MockKeystoreProvider.swift | 1 - .../NoOpCryptoProvider.swift | 1 - .../internal/BookmarkEntity+Syncable.swift | 1 - .../internal/BookmarksResponseHandler.swift | 1 - .../internal/SyncableBookmarkAdapter.swift | 1 - .../Common/MetricsEvent.swift | 1 - .../Credentials/CredentialsProvider.swift | 3 +- .../internal/CredentialsResponseHandler.swift | 1 - .../internal/SyncableCredentialsAdapter.swift | 1 - .../Settings/SettingsProvider.swift | 2 - .../EmailManager+SyncSupporting.swift | 1 - .../EmailProtectionSyncHandler.swift | 1 - .../FavoritesDisplayModeSyncHandlerBase.swift | 1 - .../SettingSyncHandler.swift | 1 - .../SettingSyncHandling.swift | 1 - .../internal/SettingsResponseHandler.swift | 1 - .../internal/SyncableSettingAdapter.swift | 1 - Sources/TestUtils/MockURLProtocol.swift | 15 +- .../Utils/HTTPURLResponseExtension.swift | 15 +- Sources/UserScript/StaticUserScript.swift | 3 +- Sources/UserScript/UserScript.swift | 1 - Sources/UserScript/UserScriptEncrypter.swift | 3 +- .../UserScript/UserScriptHostProvider.swift | 1 - Sources/UserScript/UserScriptMessage.swift | 7 +- .../UserScriptMessageEncryption.swift | 1 - Sources/UserScript/UserScriptMessaging.swift | 2 +- .../UserScript/UserScriptSourceProvider.swift | 1 - Tests/.swiftlint.yml | 15 +- .../BookmarkDatabaseCleanerTests.swift | 1 - .../BookmarksTests/BookmarkEntityTests.swift | 1 - .../BookmarkListViewModelTests.swift | 3 +- .../BookmarkMigrationTests.swift | 3 +- Tests/BookmarksTests/BookmarkUtilsTests.swift | 1 - .../BookmarkDomainsTests.swift | 1 - .../BookmarksFaviconsFetcherTests.swift | 1 - .../FaviconsFetchOperationTests.swift | 1 - .../FaviconsFetcherMocks.swift | 1 - .../FavoriteListViewModelTests.swift | 1 - .../AutofillEmailUserScriptTests.swift | 37 ++- .../Autofill/AutofillTestHelper.swift | 3 +- ...utofillUserScriptSourceProviderTests.swift | 1 - .../AutofillVaultUserScriptTests.swift | 33 ++- .../AutofillDomainNameUrlMatcherTests.swift | 1 - .../AutofillWebsiteAccountMatcherTests.swift | 3 +- .../Sort/AutofillDomainNameUrlSortTests.swift | 1 - .../AdClickAttributionCounterTests.swift | 43 ++-- .../AdClickAttributionDetectionTests.swift | 163 +++++++------ .../AdClickAttributionLogicTests.swift | 205 ++++++++-------- .../AdClickAttributionPixelTests.swift | 121 +++++----- .../AdClickAttributionRulesMutatorTests.swift | 67 +++--- ...AdClickAttributionRulesProviderTests.swift | 71 +++--- ...AdClickAttributionRulesSplitterTests.swift | 25 +- .../ContentBlockerReferenceTests.swift | 3 +- ...rRulesManagerInitialCompilationTests.swift | 47 ++-- ...lockerRulesManagerMultipleRulesTests.swift | 73 +++--- .../ContentBlockerRulesManagerTests.swift | 221 +++++++++-------- .../ContentBlockerRulesUserScriptsTests.swift | 13 +- .../ContentBlockingRulesHelper.swift | 25 +- .../ContentBlocker/DetectedRequestTests.swift | 2 +- .../DomainMatchingReportTests.swift | 11 +- .../ContentBlocker/DomainMatchingTests.swift | 19 +- .../ContentBlocker/MockWebsite.swift | 1 - .../SurrogatesReferenceTests.swift | 85 ++++--- .../SurrogatesUserScriptTests.swift | 1 - .../ContentBlocker/TestSchemeHandler.swift | 3 +- .../TrackerAllowlistReferenceTests.swift | 1 - .../TrackerDataManagerTests.swift | 25 +- .../ContentBlocker/TrackerResolverTests.swift | 189 ++++++++------- .../ContentBlocker/WebViewTestHelper.swift | 3 +- .../ContentScopePropertiesMocks.swift | 16 +- .../ContentScopePropertiesTests.swift | 1 - .../Email/EmailManagerTests.swift | 1 - .../DefaultFeatureFlaggerTests.swift | 5 +- .../DefaultInternalUserDeciderTests.swift | 7 +- .../FingerprintingReferenceTests.swift | 97 ++++---- .../GPC/GPCReferenceTests.swift | 69 +++--- .../GPC/GPCTests.swift | 15 +- .../MockInternalUserStoring.swift | 1 - .../LinkProtection/AmpMatchingTests.swift | 39 ++- .../ContentBlockerManagerMock.swift | 3 +- .../LinkProtection/URLParameterTests.swift | 27 +-- .../AdClickAttributionFeatureTests.swift | 21 +- .../AppPrivacyConfigurationTests.swift | 49 ++-- .../PrivacyConfigurationDataTests.swift | 1 - .../PrivacyConfigurationReferenceTests.swift | 21 +- .../ReferrerTrimmingTests.swift | 24 +- .../JsonToRemoteConfigModelMapperTests.swift | 1 - .../Matchers/AppAttributeMatcherTests.swift | 1 - .../DeviceAttributeMatcherTests.swift | 1 - .../Matchers/UserAttributeMatcherTests.swift | 1 - .../RangeStringMatchingAttributeTests.swift | 1 - .../RemoteMessagingConfigMatcherTests.swift | 1 - .../RemoteMessagingConfigProcessorTests.swift | 3 +- .../CredentialsDatabaseCleanerTests.swift | 1 - .../MockAutofillDatabaseProvider.swift | 4 +- .../SecureVault/SecureVaultManagerTests.swift | 169 +++++++------ .../SecureVault/SecureVaultModelTests.swift | 28 +-- ...CredentialsMigrationPerformanceTests.swift | 1 - .../SecureVaultSyncableCredentialsTests.swift | 1 - .../SecureVault/SecureVaultTests.swift | 14 +- .../BloomFilterWrapperTest.swift | 19 +- .../HTTPSUpgradeParserTests.swift | 13 +- .../HTTPSUpgradeReferenceTests.swift | 49 ++-- .../HTTPSUpgradeStoreMock.swift | 1 - .../Statistics/MockStatisticsStore.swift | 1 - .../Statistics/MockVariantManager.swift | 5 +- .../Suggestions/ScoreTests.swift | 2 +- .../Utils/JsonTestDataLoader.swift | 9 +- .../Utils/StringExtension.swift | 1 - .../XCTestManifests.swift | 18 ++ .../AppVersionExtensionTests.swift | 5 +- Tests/CommonTests/AppVersionTests.swift | 5 +- .../Concurrency/TaskTimeoutTests.swift | 3 +- .../Extensions/StringExtensionTests.swift | 26 +- Tests/CommonTests/Mocks/MockBundle.swift | 1 - Tests/CommonTests/TLD/TLDTests.swift | 15 +- .../ConfigurationFetcherTests.swift | 129 +++++----- .../Mocks/MockConfigurationURLProvider.swift | 5 +- .../ConfigurationTests/Mocks/MockStore.swift | 15 +- .../Mocks/MockValidator.swift | 3 +- Tests/DDGSyncTests/CrypterTests.swift | 14 +- .../DDGSyncTests/DDGSyncLifecycleTests.swift | 1 - Tests/DDGSyncTests/DDGSyncTests.swift | 1 - Tests/DDGSyncTests/Mocks/Mocks.swift | 1 - Tests/DDGSyncTests/SecureStorageStub.swift | 6 +- Tests/DDGSyncTests/SyncOperationTests.swift | 1 - Tests/LinuxMain.swift | 18 ++ .../ClosureNavigationResponderTests.swift | 2 +- .../DistributedNavigationDelegateTests.swift | 55 ++--- ...ibutedNavigationDelegateTestsHelpers.swift | 2 +- .../Helpers/NavigationResponderMock.swift | 4 +- .../Helpers/NavigationTestHelpers.swift | 2 +- .../Helpers/TestNavigationSchemeHandler.swift | 1 - .../NavigationRedirectsTests.swift | 2 +- .../NavigationValuesTests.swift | 2 +- .../EndpointTests.swift | 3 +- .../Mocks/NetworkProtectionServerMocks.swift | 2 +- .../NWConnectionExtensionTests.swift | 1 - ...ributedNotificationObjectCodersTests.swift | 2 +- ...LocationListCompositeRepositoryTests.swift | 7 +- .../StartupOptionTests.swift | 2 +- .../XCTestCase+TemporaryFileURL.swift | 2 +- Tests/NetworkingTests/APIRequestTests.swift | 27 +-- .../CoreDataErrorsParserTests.swift | 98 ++++---- ...DBSecureStorageDatabaseProviderTests.swift | 1 - .../SecureStorageCryptoProviderTests.swift | 1 - .../SecureVaultFactoryTests.swift | 2 +- Tests/SecureStorageTests/TestMocks.swift | 3 +- ...marksInitialSyncResponseHandlerTests.swift | 1 - .../Bookmarks/BookmarksProviderTests.swift | 1 - ...marksRegularSyncResponseHandlerTests.swift | 1 - .../SyncableBookmarkAdapterTests.swift | 1 - .../helpers/BookmarksProviderTestsBase.swift | 1 - .../helpers/SyncableBookmarksExtension.swift | 1 - ...tialsInitialSyncResponseHandlerTests.swift | 1 - .../CredentialsProviderTests.swift | 1 - ...tialsRegularSyncResponseHandlerTests.swift | 1 - .../CredentialsProviderTestsBase.swift | 1 - .../SyncableCredentialsExtension.swift | 1 - .../TestAutofillSecureVaultFactory.swift | 3 +- .../SyncDataProvidersTests/CryptingMock.swift | 1 - ...tingsInitialSyncResponseHandlerTests.swift | 1 - .../Settings/SettingsProviderTests.swift | 1 - ...tingsRegularSyncResponseHandlerTests.swift | 1 - .../helpers/SettingsProviderTestsBase.swift | 1 - .../helpers/SyncableSettingsExtension.swift | 1 - .../helpers/TestSettingSyncHandler.swift | 3 +- .../StaticUserScriptTests.swift | 1 - .../UserScriptEncrypterTests.swift | 1 - .../UserScriptMessagingTests.swift | 11 +- Tests/UserScriptTests/UserScriptTests.swift | 1 - 396 files changed, 3028 insertions(+), 2791 deletions(-) create mode 100644 Plugins/SwiftLintPlugin/InputListItem.swift create mode 100644 Plugins/SwiftLintPlugin/PathExtension.swift create mode 100644 Plugins/SwiftLintPlugin/ProcessInfo+EnvironmentType.swift create mode 100644 Plugins/SwiftLintPlugin/SwiftLintPlugin.swift diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index fb9d3160b..6a2ff0db4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: SwiftLint - uses: docker://norionomura/swiftlint:0.53.0 + uses: docker://norionomura/swiftlint:0.54.0_swift-5.9.0 with: args: swiftlint --reporter github-actions-logging --strict diff --git a/.swiftlint.yml b/.swiftlint.yml index a4eea04d7..b24e7660e 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,22 +1,82 @@ +allow_zero_lintable_files: true + disabled_rules: - - trailing_whitespace + - no_space_in_method_call + - multiple_closures_with_trailing_closure + - block_based_kvo + - compiler_protocol_init + - unused_setter_value + - line_length + - type_name + - implicit_getter + - function_parameter_count - trailing_comma - nesting + - opening_brace + +opt_in_rules: + - file_header + - explicit_init + +custom_rules: + explicit_non_final_class: + included: ".*\\.swift" + name: "Implicitly non-final class" + regex: "^\\s*(class) (?!func|var)" + capture_group: 0 + match_kinds: + - keyword + message: "Classes should be `final` by default, use explicit `internal` or `public` for non-final classes." + severity: error + enforce_os_log_wrapper: + included: ".*\\.swift" + name: "Use `import Common` for os_log instead of `import os.log`" + regex: "^(import (?:os\\.log|os|OSLog))$" + capture_group: 0 + message: "os_log wrapper ensures log args are @autoclosures (computed when needed) and to be able to use String Interpolation." + severity: error -line_length: - warning: 150 - ignores_comments: true +analyzer_rules: # Rules run by `swiftlint analyze` + - explicit_self +# Rule Config identifier_name: min_length: 1 max_length: 1000 +file_length: + warning: 1200 + error: 1200 +type_body_length: + warning: 500 + error: 500 +large_tuple: + warning: 4 + error: 5 +file_header: + required_pattern: | + \/\/ + \/\/ SWIFTLINT_CURRENT_FILENAME + \/\/ + \/\/ Copyright © \d{4} 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\. + \/\/ -type_name: - min_length: 3 - max_length: - warning: 80 - error: 100 - +# General Config excluded: + - Package.swift - .build - scripts/ + - Sources/RemoteMessaging/Model/AnyDecodable.swift + - Sources/Common/Concurrency/AsyncStream.swift + diff --git a/Package.swift b/Package.swift index 6a4b2d300..f150b03be 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,8 @@ let package = Package( .library(name: "SyncDataProviders", targets: ["SyncDataProviders"]), .library(name: "NetworkProtection", targets: ["NetworkProtection"]), .library(name: "NetworkProtectionTestUtils", targets: ["NetworkProtectionTestUtils"]), - .library(name: "SecureStorage", targets: ["SecureStorage"]) + .library(name: "SecureStorage", targets: ["SecureStorage"]), + .plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"]), ], dependencies: [ .package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "10.0.2"), @@ -41,7 +42,7 @@ let package = Package( .package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "4.52.0"), .package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"), .package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"), - .package(url: "https://github.com/duckduckgo/wireguard-apple", exact: "1.1.1") + .package(url: "https://github.com/duckduckgo/wireguard-apple", exact: "1.1.1"), ], targets: [ .target( @@ -65,12 +66,18 @@ let package = Package( ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .target( name: "Persistence", dependencies: [ "Common" - ] + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "Bookmarks", @@ -80,20 +87,27 @@ let package = Package( ], resources: [ .process("BookmarksModel.xcdatamodeld") - ] + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), - .executableTarget(name: "BookmarksTestDBBuilder", - dependencies: [ - "Bookmarks", - "Persistence" - ], - path: "Sources/BookmarksTestDBBuilder" + .executableTarget( + name: "BookmarksTestDBBuilder", + dependencies: [ + "Bookmarks", + "Persistence" + ], + path: "Sources/BookmarksTestDBBuilder", + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "BookmarksTestsUtils", dependencies: [ "Bookmarks" - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "BloomFilterObjC", @@ -106,7 +120,8 @@ let package = Package( "BloomFilterObjC" ]), .target( - name: "Crashes" + name: "Crashes", + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "DDGSync", @@ -118,7 +133,11 @@ let package = Package( resources: [ .process("SyncMetadata.xcdatamodeld"), .process("SyncPDFTemplate.png") - ] + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "Common", @@ -130,13 +149,19 @@ let package = Package( ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "ContentBlocking", dependencies: [ "TrackerRadarKit" - ]), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .target( name: "Navigation", dependencies: [ @@ -151,12 +176,18 @@ let package = Package( .define("_FRAME_HANDLE_ENABLED", .when(platforms: [.macOS])), .define("PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED", .when(platforms: [.macOS])), .define("TERMINATE_WITH_REASON_ENABLED", .when(platforms: [.macOS])), - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .target( name: "UserScript", dependencies: [ "Common" - ] + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "PrivacyDashboard", @@ -167,7 +198,11 @@ let package = Package( "ContentBlocking", .product(name: "PrivacyDashboardResources", package: "privacy-dashboard") ], - path: "Sources/PrivacyDashboard" + path: "Sources/PrivacyDashboard", + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "Configuration", @@ -175,18 +210,32 @@ let package = Package( "Networking", "BrowserServicesKit", "Common" - ]), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .target( name: "Networking", dependencies: [ "Common" - ]), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .target( name: "RemoteMessaging", dependencies: [ "Common", "BrowserServicesKit" - ] + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "SyncDataProviders", @@ -198,12 +247,19 @@ let package = Package( .product(name: "GRDB", package: "GRDB.swift"), "Persistence", "SecureStorage" - ]), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .target( name: "TestUtils", dependencies: [ "Networking" - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .target( name: "NetworkProtection", dependencies: [ @@ -213,26 +269,34 @@ let package = Package( ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .target( name: "SecureStorage", dependencies: [ "Common", .product(name: "GRDB", package: "GRDB.swift") - ] + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target( name: "SecureStorageTestsUtils", dependencies: [ "SecureStorage" - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .target(name: "WireGuardC"), .target( name: "NetworkProtectionTestUtils", dependencies: [ "NetworkProtection" - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), // MARK: - Test Targets @@ -255,8 +319,10 @@ let package = Package( .copy("Resources/Bookmarks_V3.sqlite-wal"), .copy("Resources/Bookmarks_V4.sqlite"), .copy("Resources/Bookmarks_V4.sqlite-shm"), - .copy("Resources/Bookmarks_V4.sqlite-wal") - ]), + .copy("Resources/Bookmarks_V4.sqlite-wal"), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .testTarget( name: "BrowserServicesKitTests", dependencies: [ @@ -266,28 +332,37 @@ let package = Package( ], resources: [ .copy("Resources") - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .testTarget( name: "DDGSyncTests", dependencies: [ "DDGSync" - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .testTarget( name: "DDGSyncCryptoTests", dependencies: [ .product(name: "DDGSyncCrypto", package: "sync_crypto") - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .testTarget( name: "CommonTests", dependencies: [ "Common" - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .testTarget( name: "NetworkingTests", dependencies: [ "TestUtils" - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .testTarget( name: "NavigationTests", dependencies: [ @@ -301,7 +376,9 @@ let package = Package( .define("_IS_USER_INITIATED_ENABLED", .when(platforms: [.macOS])), .define("_FRAME_HANDLE_ENABLED", .when(platforms: [.macOS])), .define("PRIVATE_NAVIGATION_DID_FINISH_CALLBACKS_ENABLED", .when(platforms: [.macOS])), - ]), + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), .testTarget( name: "UserScriptTests", dependencies: [ @@ -309,21 +386,24 @@ let package = Package( ], resources: [ .process("testUserScript.js") - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .testTarget( name: "PersistenceTests", dependencies: [ "Persistence", "TrackerRadarKit" - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .testTarget( name: "ConfigurationTests", dependencies: [ "Configuration", "TestUtils" - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .testTarget( name: "SyncDataProvidersTests", @@ -331,8 +411,21 @@ let package = Package( "BookmarksTestsUtils", "SecureStorageTestsUtils", "SyncDataProviders" + ], + plugins: [.plugin(name: "SwiftLintPlugin")] + ), + .plugin( + name: "SwiftLintPlugin", + capability: .buildTool(), + dependencies: [ + .target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])) ] ), + .binaryTarget( + name: "SwiftLintBinary", + url: "https://github.com/realm/SwiftLint/releases/download/0.54.0/SwiftLintBinary-macos.artifactbundle.zip", + checksum: "963121d6babf2bf5fd66a21ac9297e86d855cbc9d28322790646b88dceca00f1" + ), .testTarget( name: "NetworkProtectionTests", dependencies: [ @@ -343,15 +436,34 @@ let package = Package( .copy("Resources/servers-original-endpoint.json"), .copy("Resources/servers-updated-endpoint.json"), .copy("Resources/locations-endpoint.json") - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), .testTarget( name: "SecureStorageTests", dependencies: [ "SecureStorage", "SecureStorageTestsUtils" - ] + ], + plugins: [.plugin(name: "SwiftLintPlugin")] ), ], cxxLanguageStandard: .cxx11 ) + +// validate all targets have swiftlint plugin +for target in package.targets { + let targetsWithSwiftlintDisabled: Set = [ + "SwiftLintPlugin", + "SwiftLintBinary", + "BloomFilterObjC", + "BloomFilterWrapper", + "WireGuardC", + ] + guard !targetsWithSwiftlintDisabled.contains(target.name) else { continue } + guard target.plugins?.contains(where: { "\($0)" == "\(Target.PluginUsage.plugin(name: "SwiftLintPlugin"))" }) == true else { + assertionFailure("\nTarget \(target.name) is missing SwiftLintPlugin dependency.\nIf this is intended, add \"\(target.name)\" to targetsWithSwiftlintDisabled\nTarget plugins: " + + (target.plugins?.map { "\($0)" }.joined(separator: ", ") ?? "")) + continue + } +} diff --git a/Plugins/SwiftLintPlugin/InputListItem.swift b/Plugins/SwiftLintPlugin/InputListItem.swift new file mode 100644 index 000000000..8cf1a8f2b --- /dev/null +++ b/Plugins/SwiftLintPlugin/InputListItem.swift @@ -0,0 +1,36 @@ +// +// InputListItem.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 + +struct InputListItem: Codable { + + let modified: Date + private(set) var diagnostics: [String]? + + init(modified: Date) { + self.modified = modified + } + + mutating func appendDiagnosticsMessage(_ message: String) { + diagnostics?.append(message) ?? { + diagnostics = [message] + }() + } + +} diff --git a/Plugins/SwiftLintPlugin/PathExtension.swift b/Plugins/SwiftLintPlugin/PathExtension.swift new file mode 100644 index 000000000..0a0a44a4d --- /dev/null +++ b/Plugins/SwiftLintPlugin/PathExtension.swift @@ -0,0 +1,73 @@ +// +// PathExtension.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 PackagePlugin + +extension Path { + + static let mv = Path("/bin/mv") + static let echo = Path("/bin/echo") + static let cat = Path("/bin/cat") + static let sh = Path("/bin/sh") + + private static let swiftlintConfig = ".swiftlint.yml" + + /// Scans the receiver, then all of its parents looking for a configuration file with the name ".swiftlint.yml". + /// + /// - returns: Path to the configuration file, or nil if one cannot be found. + func firstParentContainingConfigFile() -> Path? { + let proposedDirectory = sequence( + first: self, + next: { path in + guard path.stem.count > 1 else { + // Check we're not at the root of this filesystem, as `removingLastComponent()` + // will continually return the root from itself. + return nil + } + + return path.removingLastComponent() + } + ).first { path in + let potentialConfigurationFile = path.appending(subpath: Self.swiftlintConfig) + return potentialConfigurationFile.isAccessible() + } + return proposedDirectory + } + + /// Safe way to check if the file is accessible from within the current process sandbox. + private func isAccessible() -> Bool { + let result = string.withCString { pointer in + access(pointer, R_OK) + } + + return result == 0 + } + + /// Get file modification date + var modified: Date { + get throws { + try FileManager.default.attributesOfItem(atPath: self.string)[.modificationDate] as? Date ?? { throw CocoaError(.fileReadUnknown) }() + } + } + + var url: URL { + URL(fileURLWithPath: self.string) + } + +} diff --git a/Plugins/SwiftLintPlugin/ProcessInfo+EnvironmentType.swift b/Plugins/SwiftLintPlugin/ProcessInfo+EnvironmentType.swift new file mode 100644 index 000000000..20a4742a5 --- /dev/null +++ b/Plugins/SwiftLintPlugin/ProcessInfo+EnvironmentType.swift @@ -0,0 +1,39 @@ +// +// ProcessInfo+EnvironmentType.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 + +extension ProcessInfo { + + enum EnvironmentType { + case xcode + case xcodebuild + case ci + } + + var environmentType: EnvironmentType { + if self.environment["GITHUB_ACTIONS"] != nil { + return .ci + } else if self.environment["UsePerConfigurationBuildLocations"] != nil { + return .xcode + } else { + return .xcodebuild + } + } + +} diff --git a/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift b/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift new file mode 100644 index 000000000..c816954e5 --- /dev/null +++ b/Plugins/SwiftLintPlugin/SwiftLintPlugin.swift @@ -0,0 +1,222 @@ +// +// SwiftLintPlugin.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 PackagePlugin + +@main +struct SwiftLintPlugin: BuildToolPlugin { + + func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { + // disable output for SPM modules built in RELEASE mode + guard let target = target as? SourceModuleTarget else { + assertionFailure("invalid target") + return [] + } + + guard (target as? SwiftSourceModuleTarget)?.compilationConditions.contains(.debug) != false || target.kind == .test else { + print("SwiftLint: \(target.name): Skipping for RELEASE build") + return [] + } + + let inputFiles = target.sourceFiles(withSuffix: "swift").map(\.path) + guard !inputFiles.isEmpty else { + print("SwiftLint: \(target.name): No input files") + return [] + } + + return try createBuildCommands( + target: target.name, + inputFiles: inputFiles, + packageDirectory: context.package.directory.firstParentContainingConfigFile() ?? context.package.directory, + workingDirectory: context.pluginWorkDirectory, + tool: context.tool(named:) + ) + } + + // swiftlint:disable function_body_length + private func createBuildCommands( + target: String, + inputFiles: [Path], + packageDirectory: Path, + workingDirectory: Path, + tool: (String) throws -> PluginContext.Tool + ) throws -> [Command] { + + // only lint when built from Xcode (disable for CI or xcodebuild) + guard case .xcode = ProcessInfo().environmentType else { return [] } + + let fm = FileManager() + + let cacheURL = URL(fileURLWithPath: workingDirectory.appending("cache.json").string) + let outputPath = workingDirectory.appending("output.txt").string + + // if clean build: clear cache + let buildDir = workingDirectory.removingLastComponent() // BrowserServicesKit + .removingLastComponent() // browserserviceskit.output + .removingLastComponent() // plugins + .removingLastComponent() // SourcePackages + .removingLastComponent() // DerivedData/DuckDuckGo-xxxx + .appending("Build") + if let buildDirContents = try? fm.contentsOfDirectory(atPath: buildDir.string), + !buildDirContents.contains("Products") { + print("SwiftLint: \(target): Clean Build") + + try? fm.removeItem(at: cacheURL) + try? fm.removeItem(atPath: outputPath) + } + + // read cached data + var cache = (try? JSONDecoder().decode([String: InputListItem].self, from: Data(contentsOf: cacheURL))) ?? [:] + // read diagnostics from last pass + let lastOutput = cache.isEmpty ? "" : (try? String(contentsOfFile: outputPath)) ?? { + // no diagnostics file – reset + cache = [:] + return "" + }() + + // analyze new/modified files and output cached diagnostics for non-modified files + var filesToProcess = Set() + var newCache = [String: InputListItem]() + for inputFile in inputFiles { + try autoreleasepool { + + let modified = try inputFile.modified + if let cacheItem = cache[inputFile.string], modified == cacheItem.modified { + // file not modified + newCache[inputFile.string] = cacheItem + return + } + + // updated modification date in cache and re-process + newCache[inputFile.string] = .init(modified: modified) + + filesToProcess.insert(inputFile.string) + } + } + + // merge diagnostics from last linter pass into cache + for outputLint in lastOutput.split(separator: "\n") { + guard let filePath = outputLint.split(separator: ":", maxSplits: 1).first.map(String.init), + !filesToProcess.contains(filePath) else { continue } + + newCache[filePath]?.appendDiagnosticsMessage(String(outputLint)) + } + + // collect cached diagnostic messages from cache + let cachedDiagnostics = newCache.values.reduce(into: [String]()) { + $0 += $1.diagnostics ?? [] + } + + // We are not producing output files and this is needed only to not include cache files into bundle + let outputFilesDirectory = workingDirectory.appending("Output") + try? fm.createDirectory(at: outputFilesDirectory.url, withIntermediateDirectories: true) + try? fm.removeItem(at: cacheURL.appendingPathExtension("tmp")) + try? fm.removeItem(atPath: outputPath + ".tmp") + + var result = [Command]() + if !filesToProcess.isEmpty { + print("SwiftLint: \(target): Processing \(filesToProcess.count) files") + + // write updated cache into temporary file, cache file will be overwritten when linting completes + try JSONEncoder().encode(newCache).write(to: cacheURL.appendingPathExtension("tmp")) + + let swiftlint = try tool("swiftlint").path + let lintCommand = """ + cd "\(packageDirectory)" && "\(swiftlint)" lint --quiet --force-exclude --cache-path "\(workingDirectory)" \ + \(filesToProcess.map { "\"\($0)\"" }.joined(separator: " ")) \ + | tee -a "\(outputPath).tmp" + """ + + result = [ + .prebuildCommand( + displayName: "\(target): SwiftLint", + executable: .sh, + arguments: ["-c", lintCommand], + outputFilesDirectory: outputFilesDirectory + ) + ] + + } else { + print("SwiftLint: \(target): No new files to process") + try JSONEncoder().encode(newCache).write(to: cacheURL) + try "".write(toFile: outputPath, atomically: false, encoding: .utf8) + } + + // output cached diagnostic messages from previous run + result.append(.prebuildCommand( + displayName: "SwiftLint: \(target): cached \(cacheURL.path)", + executable: .echo, + arguments: [cachedDiagnostics.joined(separator: "\n")], + outputFilesDirectory: outputFilesDirectory + )) + + if !filesToProcess.isEmpty { + // when ready put temporary cache and output into place + result.append(.prebuildCommand( + displayName: "SwiftLint: \(target): Cache results", + executable: .mv, + arguments: ["\(outputPath).tmp", outputPath], + outputFilesDirectory: outputFilesDirectory + )) + result.append(.prebuildCommand( + displayName: "SwiftLint: \(target): Cache source files modification dates", + executable: .mv, + arguments: [cacheURL.appendingPathExtension("tmp").path, cacheURL.path], + outputFilesDirectory: outputFilesDirectory + )) + } + + return result + } + // swiftlint:enable function_body_length + +} + +#if canImport(XcodeProjectPlugin) + +import XcodeProjectPlugin + +extension SwiftLintPlugin: XcodeBuildToolPlugin { + func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { + let inputFiles = target.inputFiles.filter { + $0.type == .source && $0.path.extension == "swift" + }.map(\.path) + + guard !inputFiles.isEmpty else { + print("SwiftLint: \(target): No input files") + return [] + } + + return try createBuildCommands( + target: target.displayName, + inputFiles: inputFiles, + packageDirectory: context.xcodeProject.directory, + workingDirectory: context.pluginWorkDirectory, + tool: context.tool(named:) + ) + } +} + +#endif + +extension String { + static let swiftlintConfigFileName = ".swiftlint.yml" + + static let debug = "DEBUG" +} diff --git a/Sources/Bookmarks/BookmarkEditorViewModel.swift b/Sources/Bookmarks/BookmarkEditorViewModel.swift index c680380b3..08244c200 100644 --- a/Sources/Bookmarks/BookmarkEditorViewModel.swift +++ b/Sources/Bookmarks/BookmarkEditorViewModel.swift @@ -65,7 +65,7 @@ public class BookmarkEditorViewModel: ObservableObject { bookmarksDatabase: CoreDataDatabase, favoritesDisplayMode: FavoritesDisplayMode, errorEvents: EventMapping?) { - + externalUpdates = subject.eraseToAnyPublisher() self.errorEvents = errorEvents self.context = bookmarksDatabase.makeContext(concurrencyType: .mainQueueConcurrencyType) @@ -87,12 +87,12 @@ public class BookmarkEditorViewModel: ObservableObject { self.observer = nil } } - + public init(creatingFolderWithParentID parentFolderID: NSManagedObjectID?, bookmarksDatabase: CoreDataDatabase, favoritesDisplayMode: FavoritesDisplayMode, errorEvents: EventMapping?) { - + externalUpdates = subject.eraseToAnyPublisher() self.errorEvents = errorEvents self.context = bookmarksDatabase.makeContext(concurrencyType: .mainQueueConcurrencyType) @@ -105,7 +105,7 @@ public class BookmarkEditorViewModel: ObservableObject { parent = BookmarkUtils.fetchRootFolder(context) } assert(parent != nil) - + // We don't support creating bookmarks from scratch at this time, so it must be a folder self.bookmark = BookmarkEntity.makeFolder(title: "", parent: parent!, @@ -152,7 +152,7 @@ public class BookmarkEditorViewModel: ObservableObject { func descendInto(_ folders: [BookmarkEntity], depth: Int) { folders.forEach { entity in - if entity.isFolder, + if entity.isFolder, entity.uuid != bookmark.uuid { locations.append(Location(bookmark: entity, depth: depth)) diff --git a/Sources/Bookmarks/BookmarkEntity.swift b/Sources/Bookmarks/BookmarkEntity.swift index 23b2f6e0f..41dd930b3 100644 --- a/Sources/Bookmarks/BookmarkEntity.swift +++ b/Sources/Bookmarks/BookmarkEntity.swift @@ -1,6 +1,5 @@ // // BookmarkEntity.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -35,7 +34,7 @@ public enum FavoritesFolderID: String, CaseIterable { @objc(BookmarkEntity) public class BookmarkEntity: NSManagedObject { - + public enum Constants { public static let rootFolderID = "bookmarks_root" public static let favoriteFoldersIDs: Set = Set(FavoritesFolderID.allCases.map(\.rawValue)) @@ -51,11 +50,11 @@ public class BookmarkEntity: NSManagedObject { case invalidFavoritesFolder case invalidFavoritesStatus } - + @nonobjc public class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "BookmarkEntity") } - + public class func entity(in context: NSManagedObjectContext) -> NSEntityDescription { return NSEntityDescription.entity(forEntityName: "BookmarkEntity", in: context)! } @@ -87,10 +86,10 @@ public class BookmarkEntity: NSManagedObject { self.init(entity: BookmarkEntity.entity(in: moc), insertInto: moc) } - + public override func awakeFromInsert() { super.awakeFromInsert() - + uuid = UUID().uuidString } @@ -127,16 +126,16 @@ public class BookmarkEntity: NSManagedObject { try super.validateForUpdate() try validate() } - + public var urlObject: URL? { guard let url = url else { return nil } return url.isBookmarklet() ? url.toEncodedBookmarklet() : URL(string: url) } - + public var isRoot: Bool { uuid == Constants.rootFolderID } - + public var childrenArray: [BookmarkEntity] { let children = children?.array as? [BookmarkEntity] ?? [] return children.filter { $0.isPendingDeletion == false } @@ -174,7 +173,7 @@ public class BookmarkEntity: NSManagedObject { let object = BookmarkEntity(context: context) object.title = title object.isFolder = true - + if insertAtBeginning { parent.insertIntoChildren(object, at: 0) } else { @@ -182,7 +181,7 @@ public class BookmarkEntity: NSManagedObject { } return object } - + public static func makeBookmark(title: String, url: String, parent: BookmarkEntity, @@ -192,7 +191,7 @@ public class BookmarkEntity: NSManagedObject { object.title = title object.url = url object.isFolder = false - + if insertAtBeginning { parent.insertIntoChildren(object, at: 0) } else { @@ -200,7 +199,7 @@ public class BookmarkEntity: NSManagedObject { } return object } - + // If `insertAt` is nil, it is inserted at the end. public func addToFavorites(insertAt: Int? = nil, favoritesRoot root: BookmarkEntity) { @@ -325,7 +324,7 @@ extension BookmarkEntity { // MARK: Generated accessors for favorites extension BookmarkEntity { - + @objc(insertObject:inFavoritesAtIndex:) @NSManaged private func insertIntoFavorites(_ value: BookmarkEntity, at idx: Int) @@ -340,7 +339,7 @@ extension BookmarkEntity { @objc(removeFavorites:) @NSManaged private func removeFromFavorites(_ values: NSOrderedSet) - + } // MARK: Generated accessors for favoriteFolders diff --git a/Sources/Bookmarks/BookmarkErrors.swift b/Sources/Bookmarks/BookmarkErrors.swift index 4b526e537..0d8c81f11 100644 --- a/Sources/Bookmarks/BookmarkErrors.swift +++ b/Sources/Bookmarks/BookmarkErrors.swift @@ -1,6 +1,6 @@ // // BookmarkErrors.swift -// +// // Copyright © 2022 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,30 +23,30 @@ public enum BookmarksCoreDataError: Error { } public enum BookmarksModelError: Error, Equatable { - + public enum ObjectType: String { case favorite case bookmark } - + public enum ModelType: String { case favorites case bookmarks case menu case edit } - + case fetchingRootItemFailed(ModelType) case saveFailed(ModelType) case indexOutOfRange(ModelType) - + case missingParent(ObjectType) - + case bookmarkFolderExpected case bookmarksListMissingFolder case bookmarksListIndexNotMatchingBookmark case favoritesListIndexNotMatchingBookmark case orphanedBookmarksPresent - + case editorNewParentMissing } diff --git a/Sources/Bookmarks/BookmarkListViewModel.swift b/Sources/Bookmarks/BookmarkListViewModel.swift index a2bb9a3d2..8e1edb50a 100644 --- a/Sources/Bookmarks/BookmarkListViewModel.swift +++ b/Sources/Bookmarks/BookmarkListViewModel.swift @@ -1,6 +1,6 @@ // // BookmarkListViewModel.swift -// +// // Copyright © 2021 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,14 +25,14 @@ import Persistence public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject { public let currentFolder: BookmarkEntity? - + let context: NSManagedObjectContext public var favoritesDisplayMode: FavoritesDisplayMode { didSet { reloadData() } } - + public var bookmarks = [BookmarkEntity]() private var observer: NSObjectProtocol? @@ -42,7 +42,7 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject { public var localUpdates: AnyPublisher private let errorEvents: EventMapping? - + public init(bookmarksDatabase: CoreDataDatabase, parentID: NSManagedObjectID?, favoritesDisplayMode: FavoritesDisplayMode, @@ -71,7 +71,7 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject { } self.bookmarks = fetchBookmarksInFolder(currentFolder) - + registerForChanges() } @@ -98,7 +98,7 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject { } save() } - + private func registerForChanges() { observer = NotificationCenter.default.addObserver(forName: NSManagedObjectContext.didSaveObjectsNotification, object: nil, @@ -232,7 +232,7 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject { errorEvents?.fire(.saveFailed(.bookmarks), error: error) } } - + // MARK: - Read public func countBookmarksForDomain(_ domain: String) -> Int { @@ -259,7 +259,7 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject { #keyPath(BookmarkEntity.isFolder), #keyPath(BookmarkEntity.isPendingDeletion) ) - + return (try? context.count(for: countRequest)) ?? 0 } diff --git a/Sources/Bookmarks/BookmarkUtils.swift b/Sources/Bookmarks/BookmarkUtils.swift index d08a6c671..089ba7105 100644 --- a/Sources/Bookmarks/BookmarkUtils.swift +++ b/Sources/Bookmarks/BookmarkUtils.swift @@ -1,5 +1,5 @@ // -// +// BookmarkUtils.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -20,13 +20,13 @@ import Foundation import CoreData public struct BookmarkUtils { - + public static func fetchRootFolder(_ context: NSManagedObjectContext) -> BookmarkEntity? { let request = BookmarkEntity.fetchRequest() request.predicate = NSPredicate(format: "%K == %@", #keyPath(BookmarkEntity.uuid), BookmarkEntity.Constants.rootFolderID) request.returnsObjectsAsFaults = false request.fetchLimit = 1 - + return try? context.fetch(request).first } @@ -138,7 +138,7 @@ public struct BookmarkUtils { request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [urlPredicate, predicate]) request.returnsObjectsAsFaults = false request.fetchLimit = 1 - + return try? context.fetch(request).first } diff --git a/Sources/Bookmarks/BookmarksDatabaseCleaner.swift b/Sources/Bookmarks/BookmarksDatabaseCleaner.swift index 61a596676..ba76c8e0e 100644 --- a/Sources/Bookmarks/BookmarksDatabaseCleaner.swift +++ b/Sources/Bookmarks/BookmarksDatabaseCleaner.swift @@ -1,6 +1,5 @@ // -// BookmarkDatabaseCleaner.swift -// DuckDuckGo +// BookmarksDatabaseCleaner.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Bookmarks/BookmarksModel.swift b/Sources/Bookmarks/BookmarksModel.swift index 85084c283..79ea9edaa 100644 --- a/Sources/Bookmarks/BookmarksModel.swift +++ b/Sources/Bookmarks/BookmarksModel.swift @@ -35,7 +35,7 @@ public protocol BookmarkListInteracting: BookmarkStoring, AnyObject { var currentFolder: BookmarkEntity? { get } var bookmarks: [BookmarkEntity] { get } var totalBookmarksCount: Int { get } - + func bookmark(at index: Int) -> BookmarkEntity? func bookmark(with id: NSManagedObjectID) -> BookmarkEntity? @@ -74,9 +74,9 @@ public protocol MenuBookmarksInteracting { var favoritesDisplayMode: FavoritesDisplayMode { get set } func createOrToggleFavorite(title: String, url: URL) - + func createBookmark(title: String, url: URL) - + func favorite(for url: URL) -> BookmarkEntity? func bookmark(for url: URL) -> BookmarkEntity? } diff --git a/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcher.swift b/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcher.swift index ee02273eb..9a8943bec 100644 --- a/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcher.swift +++ b/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcher.swift @@ -1,6 +1,5 @@ // // BookmarksFaviconsFetcher.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcherStateStore.swift b/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcherStateStore.swift index 284ac19a7..3ea024d5c 100644 --- a/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcherStateStore.swift +++ b/Sources/Bookmarks/FaviconsFetcher/BookmarksFaviconsFetcherStateStore.swift @@ -1,6 +1,5 @@ // // BookmarksFaviconsFetcherStateStore.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift b/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift index 970fed75d..0009ac189 100644 --- a/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift +++ b/Sources/Bookmarks/FaviconsFetcher/FaviconFetcher.swift @@ -1,6 +1,5 @@ // // FaviconFetcher.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Bookmarks/FaviconsFetcher/FaviconsFetchOperation.swift b/Sources/Bookmarks/FaviconsFetcher/FaviconsFetchOperation.swift index 8f21a44ac..161aed5c8 100644 --- a/Sources/Bookmarks/FaviconsFetcher/FaviconsFetchOperation.swift +++ b/Sources/Bookmarks/FaviconsFetcher/FaviconsFetchOperation.swift @@ -1,6 +1,5 @@ // // FaviconsFetchOperation.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Bookmarks/FavoriteListViewModel.swift b/Sources/Bookmarks/FavoriteListViewModel.swift index f5dfbe33b..fa56b5457 100644 --- a/Sources/Bookmarks/FavoriteListViewModel.swift +++ b/Sources/Bookmarks/FavoriteListViewModel.swift @@ -1,5 +1,5 @@ // -// FavoritesListViewModel.swift +// FavoriteListViewModel.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -23,7 +23,7 @@ import Persistence import Common public class FavoritesListViewModel: FavoritesListInteracting, ObservableObject { - + let context: NSManagedObjectContext public var favorites = [BookmarkEntity]() @@ -75,7 +75,7 @@ public class FavoritesListViewModel: FavoritesListInteracting, ObservableObject self.observer = nil } } - + private func registerForChanges() { observer = NotificationCenter.default.addObserver(forName: NSManagedObjectContext.didSaveObjectsNotification, object: nil, @@ -105,7 +105,7 @@ public class FavoritesListViewModel: FavoritesListInteracting, ObservableObject favorites = [] return } - + readFavorites(with: favoriteFolder) } @@ -114,7 +114,7 @@ public class FavoritesListViewModel: FavoritesListInteracting, ObservableObject errorEvents?.fire(.indexOutOfRange(.favorites)) return nil } - + return favorites[index] } @@ -127,10 +127,10 @@ public class FavoritesListViewModel: FavoritesListInteracting, ObservableObject favorite.removeFromFavorites(with: favoritesDisplayMode) save() - + readFavorites(with: favoriteFolder) } - + public func moveFavorite(_ favorite: BookmarkEntity, fromIndex: Int, toIndex: Int) { @@ -138,7 +138,7 @@ public class FavoritesListViewModel: FavoritesListInteracting, ObservableObject errorEvents?.fire(.fetchingRootItemFailed(.favorites)) return } - + let visibleChildren = favoriteFolder.favoritesArray guard fromIndex < visibleChildren.count, @@ -146,12 +146,12 @@ public class FavoritesListViewModel: FavoritesListInteracting, ObservableObject errorEvents?.fire(.indexOutOfRange(.favorites)) return } - + guard visibleChildren[fromIndex] == favorite else { errorEvents?.fire(.favoritesListIndexNotMatchingBookmark) return } - + // Take into account bookmarks that are pending deletion let mutableChildrenSet = favoriteFolder.mutableOrderedSetValue(forKeyPath: #keyPath(BookmarkEntity.favorites)) @@ -165,12 +165,12 @@ public class FavoritesListViewModel: FavoritesListInteracting, ObservableObject } mutableChildrenSet.moveObjects(at: IndexSet(integer: actualFromIndex), to: actualToIndex) - + save() - + readFavorites(with: favoriteFolder) } - + private func save() { do { try context.save() diff --git a/Sources/Bookmarks/FavoritesDisplayMode.swift b/Sources/Bookmarks/FavoritesDisplayMode.swift index 8c1058857..ea17b92a0 100644 --- a/Sources/Bookmarks/FavoritesDisplayMode.swift +++ b/Sources/Bookmarks/FavoritesDisplayMode.swift @@ -1,6 +1,5 @@ // // FavoritesDisplayMode.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Bookmarks/ImportExport/BookmarkCoreDataImporter.swift b/Sources/Bookmarks/ImportExport/BookmarkCoreDataImporter.swift index 4a9202039..b4f04154d 100644 --- a/Sources/Bookmarks/ImportExport/BookmarkCoreDataImporter.swift +++ b/Sources/Bookmarks/ImportExport/BookmarkCoreDataImporter.swift @@ -1,6 +1,6 @@ // // BookmarkCoreDataImporter.swift -// +// // Copyright © 2022 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,29 +21,29 @@ import CoreData import Persistence public class BookmarkCoreDataImporter { - + let context: NSManagedObjectContext let favoritesDisplayMode: FavoritesDisplayMode - + public init(database: CoreDataDatabase, favoritesDisplayMode: FavoritesDisplayMode) { self.context = database.makeContext(concurrencyType: .privateQueueConcurrencyType) self.favoritesDisplayMode = favoritesDisplayMode } - + public func importBookmarks(_ bookmarks: [BookmarkOrFolder]) async throws { - + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - - context.performAndWait { () -> Void in + + context.performAndWait { () in do { let favoritesFolders = BookmarkUtils.fetchFavoritesFolders(for: favoritesDisplayMode, in: context) guard let topLevelBookmarksFolder = BookmarkUtils.fetchRootFolder(context) else { throw BookmarksCoreDataError.fetchingExistingItemFailed } - + var bookmarkURLToIDMap = try bookmarkURLToID(in: context) - + try recursivelyCreateEntities(from: bookmarks, parent: topLevelBookmarksFolder, favoritesFolders: favoritesFolders, @@ -56,7 +56,7 @@ public class BookmarkCoreDataImporter { } } } - + private func bookmarkURLToID(in context: NSManagedObjectContext) throws -> [String: NSManagedObjectID] { let fetch = NSFetchRequest(entityName: "BookmarkEntity") fetch.predicate = NSPredicate( @@ -65,20 +65,20 @@ public class BookmarkCoreDataImporter { #keyPath(BookmarkEntity.isPendingDeletion) ) fetch.resultType = .dictionaryResultType - + let idDescription = NSExpressionDescription() idDescription.name = "objectID" idDescription.expression = NSExpression.expressionForEvaluatedObject() idDescription.expressionResultType = .objectIDAttributeType - + fetch.propertiesToFetch = [idDescription, #keyPath(BookmarkEntity.url)] - + let dict = try context.fetch(fetch) as? [Dictionary] - + if let result = dict?.reduce(into: [String: NSManagedObjectID](), { partialResult, data in guard let urlString = data[#keyPath(BookmarkEntity.url)] as? String, let objectID = data["objectID"] as? NSManagedObjectID else { return } - + partialResult[urlString] = objectID }) { return result @@ -136,7 +136,7 @@ public class BookmarkCoreDataImporter { } } } - + private func containsBookmark(with url: URL) -> Bool { return false } diff --git a/Sources/Bookmarks/ImportExport/BookmarkOrFolder.swift b/Sources/Bookmarks/ImportExport/BookmarkOrFolder.swift index 21246c3b8..57d14c5e9 100644 --- a/Sources/Bookmarks/ImportExport/BookmarkOrFolder.swift +++ b/Sources/Bookmarks/ImportExport/BookmarkOrFolder.swift @@ -1,6 +1,5 @@ // // BookmarkOrFolder.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/Bookmarks/MenuBookmarksViewModel.swift b/Sources/Bookmarks/MenuBookmarksViewModel.swift index edb25fcad..0fd251068 100644 --- a/Sources/Bookmarks/MenuBookmarksViewModel.swift +++ b/Sources/Bookmarks/MenuBookmarksViewModel.swift @@ -22,7 +22,7 @@ import Common import Persistence public class MenuBookmarksViewModel: MenuBookmarksInteracting { - + let context: NSManagedObjectContext public var favoritesDisplayMode: FavoritesDisplayMode = .displayNative(.mobile) { didSet { @@ -34,19 +34,19 @@ public class MenuBookmarksViewModel: MenuBookmarksInteracting { private var rootFolder: BookmarkEntity? { if _rootFolder == nil { _rootFolder = BookmarkUtils.fetchRootFolder(context) - + if _rootFolder == nil { errorEvents?.fire(.fetchingRootItemFailed(.menu)) } } return _rootFolder } - + private var _favoritesFolder: BookmarkEntity? private var favoritesFolder: BookmarkEntity? { if _favoritesFolder == nil { _favoritesFolder = BookmarkUtils.fetchFavoritesFolder(withUUID: favoritesDisplayMode.displayedFolder.rawValue, in: context) - + if _favoritesFolder == nil { errorEvents?.fire(.fetchingRootItemFailed(.menu)) } @@ -55,9 +55,9 @@ public class MenuBookmarksViewModel: MenuBookmarksInteracting { } private var observer: NSObjectProtocol? - + private let errorEvents: EventMapping? - + public init(bookmarksDatabase: CoreDataDatabase, errorEvents: EventMapping?) { self.errorEvents = errorEvents self.context = bookmarksDatabase.makeContext(concurrencyType: .mainQueueConcurrencyType) @@ -94,14 +94,14 @@ public class MenuBookmarksViewModel: MenuBookmarksInteracting { errorEvents?.fire(.saveFailed(.menu), error: error) } } - + public func createOrToggleFavorite(title: String, url: URL) { guard let rootFolder = rootFolder else { return } - + let queriedBookmark = favorite(for: url) ?? bookmark(for: url) - + if let bookmark = queriedBookmark { if bookmark.isFavorite(on: favoritesDisplayMode.displayedFolder) { bookmark.removeFromFavorites(with: favoritesDisplayMode) @@ -115,10 +115,10 @@ public class MenuBookmarksViewModel: MenuBookmarksInteracting { context: context) favorite.addToFavorites(with: favoritesDisplayMode, in: context) } - + save() } - + public func createBookmark(title: String, url: URL) { guard let rootFolder = rootFolder else { return @@ -129,7 +129,7 @@ public class MenuBookmarksViewModel: MenuBookmarksInteracting { context: context) save() } - + public func favorite(for url: URL) -> BookmarkEntity? { guard let favoritesFolder else { return nil @@ -143,9 +143,9 @@ public class MenuBookmarksViewModel: MenuBookmarksInteracting { ), context: context) } - + public func bookmark(for url: URL) -> BookmarkEntity? { BookmarkUtils.fetchBookmark(for: url, context: context) } - + } diff --git a/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift b/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift index 5a7745764..f8d170d13 100644 --- a/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift +++ b/Sources/Bookmarks/Migrations/BookmarkFormFactorFavoritesMigration.swift @@ -1,5 +1,7 @@ // -// Copyright © 2023 DuckDuckGo. All rights reserved. +// BookmarkFormFactorFavoritesMigration.swift +// +// Copyright © 2021 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. @@ -31,7 +33,7 @@ public class BookmarkFormFactorFavoritesMigration { guard let metadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: dbFileURL), let latestModel = CoreDataDatabase.loadModel(from: bundle, named: "BookmarksModel"), - !latestModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + !latestModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) else { return nil } diff --git a/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift b/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift index f452fb3e5..1318a072d 100644 --- a/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift +++ b/Sources/BookmarksTestDBBuilder/BookmarksTestDBBuilder.swift @@ -1,6 +1,5 @@ // // BookmarksTestDBBuilder.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BookmarksTestsUtils/BookmarkTree.swift b/Sources/BookmarksTestsUtils/BookmarkTree.swift index dd0b538e4..1991dbb65 100644 --- a/Sources/BookmarksTestsUtils/BookmarkTree.swift +++ b/Sources/BookmarksTestsUtils/BookmarkTree.swift @@ -1,6 +1,5 @@ // // BookmarkTree.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -210,7 +209,6 @@ public struct BookmarkTree { return (rootFolder, orphans) } - // swiftlint:disable large_tuple @discardableResult public func createEntitiesForCheckingModifiedAt(in context: NSManagedObjectContext) -> (BookmarkEntity, [BookmarkEntity], [String: ModifiedAtConstraint]) { let rootFolder = BookmarkUtils.fetchRootFolder(context)! @@ -236,7 +234,6 @@ public struct BookmarkTree { } return (rootFolder, orphans, modifiedAtConstraints) } - // swiftlint:enable large_tuple let modifiedAt: Date? let lastChildrenArrayReceivedFromSync: [String]? diff --git a/Sources/BookmarksTestsUtils/ModelAccessHelper.swift b/Sources/BookmarksTestsUtils/ModelAccessHelper.swift index 8b6a8a01c..099a989f8 100644 --- a/Sources/BookmarksTestsUtils/ModelAccessHelper.swift +++ b/Sources/BookmarksTestsUtils/ModelAccessHelper.swift @@ -1,6 +1,5 @@ // // ModelAccessHelper.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+Email.swift b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+Email.swift index aa1b51b49..ebc87d4af 100644 --- a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+Email.swift +++ b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+Email.swift @@ -1,6 +1,5 @@ // // AutofillUserScript+Email.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -213,7 +212,7 @@ extension AutofillUserScript { } func closeEmailProtectionTab(_ message: UserScriptMessage, replyHandler: @escaping MessageReplyHandler) { - emailDelegate?.autofillUserScriptDidCompleteInContextSignup(self) + emailDelegate?.autofillUserScriptDidCompleteInContextSignup(self) replyHandler(nil) } diff --git a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift index 2acd30a95..e3a00c4bf 100644 --- a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift +++ b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift @@ -1,6 +1,5 @@ // // AutofillUserScript+SecureVault.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -21,8 +20,6 @@ import WebKit import Common import UserScript -// swiftlint:disable line_length file_length - public enum RequestVaultCredentialsAction: String, Codable { case none case fill @@ -50,7 +47,7 @@ public protocol AutofillSecureVaultDelegate: AnyObject { subType: AutofillUserScript.GetAutofillDataSubType, trigger: AutofillUserScript.GetTriggerType, completionHandler: @escaping (SecureVaultModels.WebsiteCredentials?, SecureVaultModels.CredentialsProvider, RequestVaultCredentialsAction) -> Void) - + func autofillUserScript(_: AutofillUserScript, didRequestCredentialsForAccount accountId: String, completionHandler: @escaping (SecureVaultModels.WebsiteCredentials?, SecureVaultModels.CredentialsProvider) -> Void) func autofillUserScript(_: AutofillUserScript, didRequestCreditCardWithId creditCardId: Int64, @@ -185,9 +182,9 @@ extension AutofillUserScript { self.origin = credentialsProvider == SecureVaultModels.CredentialsProvider.Name.bitwarden.rawValue ? nil : origin } } - + // MARK: - Requests - + public struct IncomingCredentials: Equatable { private enum Constants { @@ -196,22 +193,22 @@ extension AutofillUserScript { static let passwordKey = "password" static let autogeneratedKey = "autogenerated" } - + let username: String? let password: String? var autogenerated: Bool - + init(username: String?, password: String?, autogenerated: Bool = false) { self.username = username self.password = password self.autogenerated = autogenerated } - + init?(autofillDictionary: [String: Any]) { guard let credentialsDictionary = autofillDictionary[Constants.credentialsKey] as? [String: Any] else { return nil } - + // Usernames are optional, as the Autofill script can pass a generated password through without a corresponding username. self.init(username: credentialsDictionary[Constants.usernameKey] as? String, password: credentialsDictionary[Constants.passwordKey] as? String, @@ -219,7 +216,7 @@ extension AutofillUserScript { } } - + /// Represents the incoming Autofill data provided by the user script. /// /// Identities and Credit Cards can be converted to their final model objects directly, but credentials cannot as they have to looked up in the Secure Vault first, hence the existence of a standalone @@ -234,11 +231,11 @@ extension AutofillUserScript { public var credentials: IncomingCredentials? public let creditCard: SecureVaultModels.CreditCard? public let trigger: GetTriggerType? - + var hasAutogeneratedCredentials: Bool { return credentials?.autogenerated ?? false } - + init(dictionary: [String: Any]) { self.identity = .init(autofillDictionary: dictionary) self.creditCard = .init(autofillDictionary: dictionary) @@ -249,19 +246,18 @@ extension AutofillUserScript { self.trigger = nil } } - + init(identity: SecureVaultModels.Identity?, credentials: AutofillUserScript.IncomingCredentials?, creditCard: SecureVaultModels.CreditCard?, trigger: GetTriggerType?) { self.identity = identity self.credentials = credentials self.creditCard = creditCard self.trigger = trigger } - + } // MARK: - Responses - // swiftlint:disable nesting struct RequestAutoFillInitDataResponse: Codable { struct AutofillInitSuccess: Codable { @@ -339,8 +335,6 @@ extension AutofillUserScript { let success: IncontextSignupDismissedAt } - // swiftlint:enable nesting - struct RequestAutoFillCreditCardResponse: Codable { let success: CreditCardObject let error: String? @@ -358,7 +352,7 @@ extension AutofillUserScript { let success: [CredentialObject] } - + struct CredentialResponse: Codable { let id: String // When bitwarden is locked use id = "provider_locked" @@ -396,16 +390,15 @@ extension AutofillUserScript { } // GetAutofillDataResponse: https://github.com/duckduckgo/duckduckgo-autofill/blob/main/src/deviceApiCalls/schemas/getAutofillData.result.json - // swiftlint:disable nesting struct RequestVaultCredentialsForDomainResponse: Codable { struct RequestVaultCredentialsResponseContents: Codable { let credentials: CredentialResponse? let action: RequestVaultCredentialsAction } - + let success: RequestVaultCredentialsResponseContents - + static func responseFromSecureVaultWebsiteCredentials(_ credentials: SecureVaultModels.WebsiteCredentials?, credentialsProvider: SecureVaultModels.CredentialsProvider, action: RequestVaultCredentialsAction) -> Self { @@ -419,17 +412,15 @@ extension AutofillUserScript { } else { credential = nil } - + return RequestVaultCredentialsForDomainResponse(success: RequestVaultCredentialsResponseContents(credentials: credential, action: action)) } } - + struct RequestVaultCredentialsForAccountResponse: Codable { let success: CredentialResponse } - // swiftlint:enable nesting - // MARK: - Message Handlers func getRuntimeConfiguration(_ message: UserScriptMessage, _ replyHandler: @escaping MessageReplyHandler) { @@ -475,7 +466,7 @@ extension AutofillUserScript { case username case password } - + // https://github.com/duckduckgo/duckduckgo-autofill/blob/main/src/deviceApiCalls/schemas/getAutofillData.params.json public enum GetTriggerType: String, Codable { case userInitiated @@ -571,14 +562,14 @@ extension AutofillUserScript { defer { replyHandler(nil) } - + guard let body = message.messageBody as? [String: Any] else { return } - + let incomingData = DetectedAutofillData(dictionary: body) let domain = hostProvider.hostForMessage(message) - + vaultDelegate?.autofillUserScript(self, didRequestStoreDataForDomain: domain, data: incomingData) } @@ -599,7 +590,7 @@ extension AutofillUserScript { } func pmGetAutofillCredentials(_ message: UserScriptMessage, _ replyHandler: @escaping MessageReplyHandler) { - + guard let body = message.messageBody as? [String: Any], let id = body["id"] as? String else { return @@ -664,7 +655,7 @@ extension AutofillUserScript { } } } - + func askToUnlockProvider(_ message: UserScriptMessage, _ replyHandler: @escaping MessageReplyHandler) { let domain = hostForMessage(message) let email = emailDelegate?.autofillUserScriptDidRequestSignedInStatus(self) ?? false @@ -682,7 +673,7 @@ extension AutofillUserScript { } }) } - + // On Catalina we poll this method every x seconds from all tabs func checkCredentialsProviderStatus(_ message: UserScriptMessage, _ replyHandler: @escaping MessageReplyHandler) { if #available(macOS 11, *) { @@ -923,5 +914,3 @@ extension AutofillUserScript.AskToUnlockProviderResponse { } } - -// swiftlint:enable line_length file_length diff --git a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SourceProvider.swift b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SourceProvider.swift index ed553daf7..2487d5036 100644 --- a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SourceProvider.swift +++ b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SourceProvider.swift @@ -1,6 +1,5 @@ // // AutofillUserScript+SourceProvider.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/Autofill/AutofillUserScript.swift b/Sources/BrowserServicesKit/Autofill/AutofillUserScript.swift index 5b9ad133e..a140494c7 100644 --- a/Sources/BrowserServicesKit/Autofill/AutofillUserScript.swift +++ b/Sources/BrowserServicesKit/Autofill/AutofillUserScript.swift @@ -1,6 +1,5 @@ // // AutofillUserScript.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -51,10 +50,10 @@ public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncrypti case getAvailableInputTypes case getAutofillData case storeFormData - + case askToUnlockProvider case checkCredentialsProviderStatus - + case sendJSPixel case setIncontextSignupPermanentlyDismissedAt @@ -120,7 +119,7 @@ public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncrypti os_log("Failed to parse Autofill User Script message: '%{public}s'", log: .userScripts, type: .debug, messageName) return nil } - + os_log("AutofillUserScript: received '%{public}s'", log: .userScripts, type: .debug, messageName) switch message { @@ -148,7 +147,7 @@ public class AutofillUserScript: NSObject, UserScript, UserScriptMessageEncrypti case .pmHandlerOpenManageCreditCards: return pmOpenManageCreditCards case .pmHandlerOpenManageIdentities: return pmOpenManageIdentities case .pmHandlerOpenManagePasswords: return pmOpenManagePasswords - + case .askToUnlockProvider: return askToUnlockProvider case .checkCredentialsProviderStatus: return checkCredentialsProviderStatus diff --git a/Sources/BrowserServicesKit/Autofill/Matchers/AutofillAccountMatcher.swift b/Sources/BrowserServicesKit/Autofill/Matchers/AutofillAccountMatcher.swift index d6d977386..d59933347 100644 --- a/Sources/BrowserServicesKit/Autofill/Matchers/AutofillAccountMatcher.swift +++ b/Sources/BrowserServicesKit/Autofill/Matchers/AutofillAccountMatcher.swift @@ -1,6 +1,5 @@ // // AutofillAccountMatcher.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/Autofill/Matchers/AutofillUrlMatcher.swift b/Sources/BrowserServicesKit/Autofill/Matchers/AutofillUrlMatcher.swift index 5bce667ef..33c9479de 100644 --- a/Sources/BrowserServicesKit/Autofill/Matchers/AutofillUrlMatcher.swift +++ b/Sources/BrowserServicesKit/Autofill/Matchers/AutofillUrlMatcher.swift @@ -1,6 +1,5 @@ // // AutofillUrlMatcher.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/Autofill/OverlayAutofillUserScript.swift b/Sources/BrowserServicesKit/Autofill/OverlayAutofillUserScript.swift index e6c36c54c..43fbc780a 100644 --- a/Sources/BrowserServicesKit/Autofill/OverlayAutofillUserScript.swift +++ b/Sources/BrowserServicesKit/Autofill/OverlayAutofillUserScript.swift @@ -1,6 +1,5 @@ // // OverlayAutofillUserScript.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/Autofill/Sort/AutofillUrlSort.swift b/Sources/BrowserServicesKit/Autofill/Sort/AutofillUrlSort.swift index 5f511953e..7f371ade9 100644 --- a/Sources/BrowserServicesKit/Autofill/Sort/AutofillUrlSort.swift +++ b/Sources/BrowserServicesKit/Autofill/Sort/AutofillUrlSort.swift @@ -1,6 +1,5 @@ // // AutofillUrlSort.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/Autofill/WebsiteAutofillUserScript.swift b/Sources/BrowserServicesKit/Autofill/WebsiteAutofillUserScript.swift index 94860ad9e..7d89624be 100644 --- a/Sources/BrowserServicesKit/Autofill/WebsiteAutofillUserScript.swift +++ b/Sources/BrowserServicesKit/Autofill/WebsiteAutofillUserScript.swift @@ -1,6 +1,5 @@ // // WebsiteAutofillUserScript.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -56,7 +55,7 @@ public class WebsiteAutofillUserScript: AutofillUserScript { case closeAutofillParent case getSelectedCredentials case showAutofillParent - } + } public override func messageHandlerFor(_ messageName: String) -> MessageHandler? { guard let websiteAutofillMessageName = WebsiteAutofillMessageName(rawValue: messageName) else { @@ -92,7 +91,7 @@ public class WebsiteAutofillUserScript: AutofillUserScript { } // Sets the last message host, so we can check when it messages back lastOpenHost = hostProvider.hostForMessage(message) - + currentOverlayTab.websiteAutofillUserScript(self, willDisplayOverlayAtClick: clickPoint, serializedInputContext: serializedInputContext, diff --git a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionCounter.swift b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionCounter.swift index 57e60ffd8..8fa55c934 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionCounter.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionCounter.swift @@ -1,6 +1,5 @@ // // AdClickAttributionCounter.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,7 +21,7 @@ import Persistence /// This class aggregates detected Ad Attributions on a websites and stores that count over a certain time interval. public class AdClickAttributionCounter { - + public enum Constant { public static let pageLoadsCountKey = "AdClickAttributionCounter_Count" @@ -30,11 +29,11 @@ public class AdClickAttributionCounter { public static let sendInterval: Double = 60 * 60 * 24 // 24 hours } - + private let store: KeyValueStoring private let onSend: (_ count: Int) -> Void private let sendInterval: Double - + public init(store: KeyValueStoring = AdClickAttributionCounterStore(), sendInterval: Double = Constant.sendInterval, onSendRequest: @escaping (_ count: Int) -> Void) { @@ -42,12 +41,12 @@ public class AdClickAttributionCounter { self.onSend = onSendRequest self.sendInterval = sendInterval } - + public func onAttributionActive(currentTime: Date = Date()) { save(pageLoadsCount: pageLoadsCount + 1) sendEventsIfNeeded(currentTime: currentTime) } - + public func sendEventsIfNeeded(currentTime: Date = Date()) { guard let lastSendAt else { save(lastSendAt: currentTime) diff --git a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionCounterStore.swift b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionCounterStore.swift index b1bbe5ef4..e377ab94b 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionCounterStore.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionCounterStore.swift @@ -1,6 +1,5 @@ // -// AdClickAttributionCounter.swift -// DuckDuckGo +// AdClickAttributionCounterStore.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionDetection.swift b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionDetection.swift index 648f6266e..6b56daabc 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionDetection.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionDetection.swift @@ -1,6 +1,5 @@ // // AdClickAttributionDetection.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -21,24 +20,24 @@ import Foundation import Common public protocol AdClickAttributionDetectionDelegate: AnyObject { - + func attributionDetection(_ detection: AdClickAttributionDetection, didDetectVendor vendorHost: String) } public class AdClickAttributionDetection { - + enum State { case idle // Waiting for detection to start case detecting(String?) // Detection is in progress, parameter is vendor obtained from domain detection mechanism } - + private let attributionFeature: AdClickAttributing - + private var state = State.idle - + private let tld: TLD private let getLog: () -> OSLog private var log: OSLog { @@ -46,9 +45,9 @@ public class AdClickAttributionDetection { } private let eventReporting: EventMapping? private let errorReporting: EventMapping? - + public weak var delegate: AdClickAttributionDetectionDelegate? - + public init(feature: AdClickAttributing, tld: TLD, eventReporting: EventMapping? = nil, @@ -60,21 +59,21 @@ public class AdClickAttributionDetection { self.errorReporting = errorReporting self.getLog = log } - + // MARK: - Public API public func onStartNavigation(url: URL?) { guard attributionFeature.isEnabled, let url = url, attributionFeature.isMatchingAttributionFormat(url) else { return } - + os_log(.debug, log: log, "Starting Attribution detection for %{private}s", url.host ?? "nil") - + var vendorDomain: String? if attributionFeature.isDomainDetectionEnabled, let adDomainParameterName = attributionFeature.attributionDomainParameterName(for: url), let domainFromParameter = url.getParameter(named: adDomainParameterName), !domainFromParameter.isEmpty { - + if let eTLDp1 = tld.eTLDplus1(domainFromParameter)?.lowercased() { vendorDomain = eTLDp1 delegate?.attributionDetection(self, didDetectVendor: eTLDp1) @@ -82,45 +81,45 @@ public class AdClickAttributionDetection { errorReporting?.fire(.adAttributionDetectionInvalidDomainInParameter) } } - + if attributionFeature.isHeuristicDetectionEnabled { state = .detecting(vendorDomain) } else { fireDetectionPixel(serpBasedDomain: vendorDomain, heuristicBasedDomain: nil) } } - + public func on2XXResponse(url: URL?) { guard let host = url?.host else { return } - + heuristicDetection(forHost: host) } - + public func onDidFailNavigation() { os_log(.debug, log: log, "Attribution detection has been cancelled") state = .idle } - + public func onDidFinishNavigation(url: URL?) { guard let host = url?.host else { return } - + heuristicDetection(forHost: host) } - + // MARK: - Private functionality - + private func heuristicDetection(forHost host: String) { guard case .detecting(let domainFromParameter) = state else { return } - + os_log(.debug, log: log, "Attribution detected for %{private}s", host) state = .idle - + let detectedDomain = tld.eTLDplus1(host)?.lowercased() if domainFromParameter == nil { if let vendorDomain = detectedDomain { @@ -129,14 +128,14 @@ public class AdClickAttributionDetection { errorReporting?.fire(.adAttributionDetectionHeuristicsDidNotMatchDomain) } } - + fireDetectionPixel(serpBasedDomain: domainFromParameter, heuristicBasedDomain: detectedDomain) } - + private func fireDetectionPixel(serpBasedDomain: String?, heuristicBasedDomain: String?) { - + let domainDetection: String - + if serpBasedDomain != nil && serpBasedDomain == heuristicBasedDomain { domainDetection = "matched" } else if serpBasedDomain != nil && !attributionFeature.isHeuristicDetectionEnabled { @@ -148,7 +147,7 @@ public class AdClickAttributionDetection { } else { domainDetection = "none" } - + let parameters = [AdClickAttributionEvents.Parameters.domainDetection: domainDetection, AdClickAttributionEvents.Parameters.domainDetectionEnabled: attributionFeature.isDomainDetectionEnabled ? "1" : "0", AdClickAttributionEvents.Parameters.heuristicDetectionEnabled: attributionFeature.isHeuristicDetectionEnabled ? "1" : "0"] diff --git a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionEvents.swift b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionEvents.swift index 94c33bffb..46f24a87c 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionEvents.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionEvents.swift @@ -1,6 +1,5 @@ // -// AdClickAttributionDebugEvents.swift -// DuckDuckGo +// AdClickAttributionEvents.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -20,21 +19,21 @@ import Foundation public enum AdClickAttributionEvents { - + public enum Parameters { public static let domainDetection = "domainDetection" public static let heuristicDetectionEnabled = "heuristicDetectionEnabled" public static let domainDetectionEnabled = "domainDetectionEnabled" public static let count = "count" } - + case adAttributionDetected case adAttributionActive case adAttributionPageLoads } public enum AdClickAttributionDebugEvents { - + case adAttributionGlobalAttributedRulesDoNotExist case adAttributionCompilationFailedForAttributedRulesList case adAttributionLogicRequestingAttributionTimedOut @@ -45,5 +44,5 @@ public enum AdClickAttributionDebugEvents { case adAttributionLogicWrongVendorOnFailedCompilation case adAttributionDetectionInvalidDomainInParameter case adAttributionDetectionHeuristicsDidNotMatchDomain - + } diff --git a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionLogic.swift b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionLogic.swift index 1c0d33eab..0eb51a6a0 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionLogic.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionLogic.swift @@ -1,6 +1,5 @@ // // AdClickAttributionLogic.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,15 +21,14 @@ import ContentBlocking import Common public protocol AdClickAttributionLogicDelegate: AnyObject { - + func attributionLogic(_ logic: AdClickAttributionLogic, didRequestRuleApplication rules: ContentBlockerRulesManager.Rules?, forVendor vendor: String?) } -// swiftlint:disable:next type_body_length public class AdClickAttributionLogic { - + public enum State { case noAttribution @@ -42,19 +40,19 @@ public class AdClickAttributionLogic { return false } } - + public struct SessionInfo { // Start of the attribution public let attributionStartedAt: Date // Present when we leave webpage associated with the attribution public let leftAttributionContextAt: Date? - + init(start: Date = Date(), leftContextAt: Date? = nil) { attributionStartedAt = start leftAttributionContextAt = leftContextAt } } - + private let featureConfig: AdClickAttributing private let rulesProvider: AdClickAttributionRulesProviding private let tld: TLD @@ -71,11 +69,11 @@ public class AdClickAttributionLogic { public private(set) var state = State.noAttribution private var registerFirstActivity = false - + private var attributionTimeout: DispatchWorkItem? - + public weak var delegate: AdClickAttributionLogicDelegate? - + public init(featureConfig: AdClickAttributing, rulesProvider: AdClickAttributionRulesProviding, tld: TLD, @@ -92,12 +90,12 @@ public class AdClickAttributionLogic { public func applyInheritedAttribution(state: State?) { guard let state = state else { return } - + if case .noAttribution = self.state {} else { errorReporting?.fire(.adAttributionLogicUnexpectedStateOnInheritedAttribution) assert(NSClassFromString("XCTest") != nil /* allow when running tests */, "unexpected initial attribution state \(self.state)") } - + switch state { case .noAttribution: self.state = state @@ -111,7 +109,7 @@ public class AdClickAttributionLogic { } } } - + public func onRulesChanged(latestRules: [ContentBlockerRulesManager.Rules]) { switch state { case .noAttribution: @@ -122,18 +120,18 @@ public class AdClickAttributionLogic { requestAttribution(forVendor: vendor) } } - + public func reapplyCurrentRules() { applyRules() } - + public func onBackForwardNavigation(mainFrameURL: URL?) { guard case .activeAttribution(let vendor, let session, let rules) = state, let host = mainFrameURL?.host, let currentETLDp1 = tld.eTLDplus1(host) else { return } - + if vendor == currentETLDp1 { if session.leftAttributionContextAt != nil { state = .activeAttribution(vendor: vendor, @@ -170,7 +168,7 @@ public class AdClickAttributionLogic { completion() } } - + @MainActor public func onProvisionalNavigation() async { await withCheckedContinuation { continuation in @@ -188,13 +186,13 @@ public class AdClickAttributionLogic { if tld.eTLDplus1(host) == vendor { counter.onAttributionActive() } - + if currentTime.timeIntervalSince(session.attributionStartedAt) >= featureConfig.totalExpiration { os_log(.debug, log: log, "Attribution has expired - total expiration") disableAttribution() return } - + if let leftAttributionContextAt = session.leftAttributionContextAt { if currentTime.timeIntervalSince(leftAttributionContextAt) >= featureConfig.navigationExpiration { os_log(.debug, log: log, "Attribution has expired - navigational expiration") @@ -213,11 +211,11 @@ public class AdClickAttributionLogic { rules: rules) } } - + public func onRequestDetected(request: DetectedRequest) { guard registerFirstActivity, BlockingState.allowed(reason: .adClickAttribution) == request.state else { return } - + eventReporting?.fire(.adAttributionActive) registerFirstActivity = false } @@ -227,7 +225,7 @@ public class AdClickAttributionLogic { state = .noAttribution applyRules() } - + private func onAttributedRulesCompiled(forVendor vendor: String, _ rules: ContentBlockerRulesManager.Rules) { guard case .preparingAttribution(let expectedVendor, let session, let completionBlocks) = state else { os_log(.error, log: log, "Attributed Rules received unexpectedly") @@ -273,13 +271,13 @@ public class AdClickAttributionLogic { delegate?.attributionLogic(self, didRequestRuleApplication: rulesProvider.globalAttributionRules, forVendor: nil) } } - + /// Request attribution when we detect it is needed private func requestAttribution(forVendor vendorHost: String, attributionStartedAt: Date = Date(), completionBlocks: [() -> Void] = []) { state = .preparingAttribution(vendor: vendorHost, session: SessionInfo(start: attributionStartedAt), completionBlocks: completionBlocks) - + scheduleTimeout(forVendor: vendorHost) rulesProvider.requestAttribution(forVendor: vendorHost) { [weak self] rules in self?.cancelTimeout() @@ -290,7 +288,7 @@ public class AdClickAttributionLogic { } } } - + private func scheduleTimeout(forVendor vendor: String) { let timeoutWorkItem = DispatchWorkItem { [weak self] in self?.onAttributedRulesCompilationFailed(forVendor: vendor) @@ -303,12 +301,12 @@ public class AdClickAttributionLogic { DispatchQueue.main.asyncAfter(deadline: .now() + 4.0, execute: timeoutWorkItem) } - + private func cancelTimeout() { attributionTimeout?.cancel() attributionTimeout = nil } - + /// Respond to new requests for attribution private func onAttributionRequested(forVendor vendorHost: String) { @@ -332,16 +330,16 @@ public class AdClickAttributionLogic { } } } - + } extension AdClickAttributionLogic: AdClickAttributionDetectionDelegate { - + public func attributionDetection(_ detection: AdClickAttributionDetection, didDetectVendor vendorHost: String) { os_log(.debug, log: log, "Detected attribution requests for %{private}s", vendorHost) onAttributionRequested(forVendor: vendorHost) registerFirstActivity = true } - + } diff --git a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesMutator.swift b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesMutator.swift index 622997349..dac883060 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesMutator.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesMutator.swift @@ -1,6 +1,5 @@ // // AdClickAttributionRulesMutator.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -21,29 +20,29 @@ import TrackerRadarKit import Foundation public class AdClickAttributionRulesMutator { - + var trackerData: TrackerData var config: AdClickAttributing - + public init(trackerData: TrackerData, config: AdClickAttributing) { self.trackerData = trackerData self.config = config } - + public func addException(vendorDomain: String) -> TrackerData { guard config.isEnabled else { return trackerData } - + let attributedMatching = KnownTracker.Rule.Matching(domains: [vendorDomain.droppingWwwPrefix()], types: nil) - + var attributedTrackers = [TrackerData.TrackerDomain: KnownTracker]() - + for (entity, tracker) in trackerData.trackers { let allowlistEntries = config.allowlist.filter { $0.entity == entity } guard !allowlistEntries.isEmpty else { attributedTrackers[entity] = tracker continue } - + var updatedRules = tracker.rules ?? [] for allowlistEntry in allowlistEntries { updatedRules.insert(KnownTracker.Rule(rule: normalizeRule(allowlistEntry.host), @@ -53,7 +52,7 @@ public class AdClickAttributionRulesMutator { exceptions: attributedMatching), at: 0) } - + attributedTrackers[entity] = KnownTracker(domain: tracker.domain, defaultAction: tracker.defaultAction, owner: tracker.owner, @@ -62,13 +61,13 @@ public class AdClickAttributionRulesMutator { categories: tracker.categories, rules: updatedRules) } - + return TrackerData(trackers: attributedTrackers, entities: trackerData.entities, domains: trackerData.domains, cnames: trackerData.cnames) } - + private func normalizeRule(_ rule: String) -> String { var rule = rule.hasSuffix("/") ? rule : rule + "/" let index = rule.firstIndex(of: "/") diff --git a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesProvider.swift b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesProvider.swift index 6d4baa4ba..11a03971e 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesProvider.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesProvider.swift @@ -1,6 +1,5 @@ // -// AdClickAttributionRulesSource.swift -// DuckDuckGo +// AdClickAttributionRulesProvider.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,40 +21,40 @@ import TrackerRadarKit import Common public protocol AdClickAttributionRulesProviding { - + var globalAttributionRules: ContentBlockerRulesManager.Rules? { get } func requestAttribution(forVendor vendor: String, completion: @escaping (ContentBlockerRulesManager.Rules?) -> Void) } public class AdClickAttributionRulesProvider: AdClickAttributionRulesProviding { - + public enum Constants { public static let attributedTempRuleListName = "TemporaryAttributed" } - + struct AttributionTask: Equatable { - + let sourceRulesIdentifier: String let vendor: String let completion: (ContentBlockerRulesManager.Rules?) -> Void - + static func == (lhs: AdClickAttributionRulesProvider.AttributionTask, rhs: AdClickAttributionRulesProvider.AttributionTask) -> Bool { return lhs.vendor == rhs.vendor && lhs.sourceRulesIdentifier == rhs.sourceRulesIdentifier } } - + private let attributionConfig: AdClickAttributing private let compiledRulesSource: CompiledRuleListsSource private let exceptionsSource: ContentBlockerRulesExceptionsSource private let errorReporting: EventMapping? private let compilationErrorReporting: EventMapping? - + private let lock = NSLock() private var tasks = [AttributionTask]() private var isProcessingTask = false - + private let workQueue = DispatchQueue(label: "AdAttribution compilation queue", qos: .userInitiated) private let getLog: () -> OSLog @@ -76,46 +75,46 @@ public class AdClickAttributionRulesProvider: AdClickAttributionRulesProviding { self.compilationErrorReporting = compilationErrorReporting self.getLog = log } - + public var globalAttributionRules: ContentBlockerRulesManager.Rules? { return compiledRulesSource.currentAttributionRules } - + public func requestAttribution(forVendor vendor: String, completion: @escaping (ContentBlockerRulesManager.Rules?) -> Void) { lock.lock() defer { lock.unlock() } - + os_log(.debug, log: log, "Preparing attribution rules for vendor %{private}s", vendor) - + guard let globalAttributionRules = compiledRulesSource.currentAttributionRules else { errorReporting?.fire(.adAttributionGlobalAttributedRulesDoNotExist) os_log(.error, log: log, "Global attribution list does not exist") completion(nil) return } - + let task = AttributionTask(sourceRulesIdentifier: globalAttributionRules.identifier.stringValue, vendor: vendor, completion: completion) tasks.append(task) - + workQueue.async { self.popTaskAndExecute() } } - + private func popTaskAndExecute() { lock.lock() defer { lock.unlock() } - + guard !isProcessingTask, !tasks.isEmpty else { return } - + let task = tasks.removeFirst() isProcessingTask = true prepareRules(for: task) } - + private func prepareRules(for task: AttributionTask) { guard let sourceRules = compiledRulesSource.currentAttributionRules else { isProcessingTask = false @@ -124,13 +123,13 @@ public class AdClickAttributionRulesProvider: AdClickAttributionRulesProviding { } return } - + os_log(.debug, log: log, "Compiling attribution rules for vendor %{private}s", task.vendor) - + let mutator = AdClickAttributionRulesMutator(trackerData: sourceRules.trackerData, config: attributionConfig) let attributedRules = mutator.addException(vendorDomain: task.vendor) - + let attributedDataSet = TrackerDataManager.DataSet(tds: attributedRules, etag: sourceRules.etag) let attributedRulesList = ContentBlockerRulesList(name: Constants.attributedTempRuleListName, @@ -142,48 +141,48 @@ public class AdClickAttributionRulesProvider: AdClickAttributionRulesProviding { exceptionsSource: exceptionsSource, errorReporting: compilationErrorReporting, log: log) - + let compilationTask = ContentBlockerRulesManager.CompilationTask(workQueue: workQueue, rulesList: attributedRulesList, sourceManager: sourceManager) - + compilationTask.start(ignoreCache: true) { compilationTask, _ in self.onTaskCompleted(attributionTask: task, compilationTask: compilationTask) } } - + private func onTaskCompleted(attributionTask: AttributionTask, compilationTask: ContentBlockerRulesManager.CompilationTask) { lock.lock() defer { lock.unlock() } - + isProcessingTask = false - + // Take all tasks with same parameters (rules & vendor) and report completion // This is optimization: in case multiple tabs request same attribution at the same time, we will respond quickly. var matchingTasks = tasks.filter { $0 == attributionTask } tasks.removeAll(where: { $0 == attributionTask }) matchingTasks.append(attributionTask) - + os_log(.debug, log: log, "Returning attribution rules for vendor %{private}s to %{public}d caller(s)", attributionTask.vendor, matchingTasks.count) - + var rules: ContentBlockerRulesManager.Rules? if let result = compilationTask.result { rules = .init(compilationResult: result) } - + DispatchQueue.main.async { for task in matchingTasks { task.completion(rules) } } - + if rules == nil { errorReporting?.fire(.adAttributionCompilationFailedForAttributedRulesList) } - + workQueue.async { self.popTaskAndExecute() } diff --git a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesSplitter.swift b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesSplitter.swift index 9f456fcf8..3e2b2ffd6 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesSplitter.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/AdClickAttribution/AdClickAttributionRulesSplitter.swift @@ -1,6 +1,5 @@ // // AdClickAttributionRulesSplitter.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -20,17 +19,17 @@ import TrackerRadarKit public struct AdClickAttributionRulesSplitter { - + public enum Constants { public static let attributionRuleListNamePrefix = "Attribution_" public static let attributionRuleListETagPrefix = "A_" } - + private let rulesList: ContentBlockerRulesList private let allowlistedTrackerNames: [String] - + // MARK: - API - + /// - Parameters: /// - rulesList: Rules list to be split /// - allowlistedTrackerNames: Tracker names to split by @@ -38,33 +37,33 @@ public struct AdClickAttributionRulesSplitter { self.rulesList = rulesList self.allowlistedTrackerNames = allowlistedTrackerNames } - + static public func blockingAttributionRuleListName(forListNamed name: String) -> String { return "\(Constants.attributionRuleListNamePrefix)\(name)" } - + /// - Returns: Split rules only if the input rulesList contains given tracker names to split by public func split() -> (ContentBlockerRulesList, ContentBlockerRulesList)? { guard !allowlistedTrackerNames.isEmpty, rulesList.contains(allowlistedTrackerNames) else { return nil } - + let splitTDS = rulesList.trackerData != nil ? split(tds: rulesList.trackerData!) : nil return (ContentBlockerRulesList(name: rulesList.name, trackerData: splitTDS?.0, fallbackTrackerData: split(tds: rulesList.fallbackTrackerData).0), ContentBlockerRulesList(name: Self.blockingAttributionRuleListName(forListNamed: rulesList.name), trackerData: splitTDS?.1, fallbackTrackerData: split(tds: rulesList.fallbackTrackerData).1)) } - + private func split(tds: TrackerDataManager.DataSet) -> (TrackerDataManager.DataSet, TrackerDataManager.DataSet) { let regularTrackerData = makeRegularTrackerData(from: tds.tds) let attributionTrackerData = makeTrackerDataForAttribution(from: tds.tds) - + // Tweak ETag to prevent caching issues between changed lists return ((tds: regularTrackerData, etag: Constants.attributionRuleListETagPrefix + tds.etag), (tds: attributionTrackerData, etag: Constants.attributionRuleListETagPrefix + tds.etag)) } - + private func makeRegularTrackerData(from trackerData: TrackerData) -> TrackerData { let trackers = trackerData.trackers.filter { !allowlistedTrackerNames.contains($0.key) } return TrackerData(trackers: trackers, @@ -72,18 +71,18 @@ public struct AdClickAttributionRulesSplitter { domains: trackerData.domains, cnames: trackerData.cnames) } - + private func makeTrackerDataForAttribution(from trackerData: TrackerData) -> TrackerData { let allowlistedTrackers = trackerData.trackers.filter { allowlistedTrackerNames.contains($0.key) } let allowlistedTrackersOwners = allowlistedTrackers.values.compactMap { $0.owner?.name } - + var entities = [String: Entity]() for ownerName in allowlistedTrackersOwners { if let entity = trackerData.entities[ownerName] { entities[ownerName] = entity } } - + var domains = [String: String]() for entity in entities { for domain in entity.value.domains ?? [] { @@ -92,21 +91,21 @@ public struct AdClickAttributionRulesSplitter { } return TrackerData(trackers: allowlistedTrackers, entities: entities, domains: domains, cnames: nil) } - + } private extension ContentBlockerRulesList { - + func contains(_ trackerNames: [String]) -> Bool { trackerData?.tds.contains(trackerNames) ?? false || fallbackTrackerData.tds.contains(trackerNames) } - + } private extension TrackerData { - + func contains(_ trackerNames: [String]) -> Bool { !Set(trackers.keys).isDisjoint(with: Set(trackerNames)) } - + } diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerDebugEvents.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerDebugEvents.swift index 8972a9bd9..4f7b174ee 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerDebugEvents.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerDebugEvents.swift @@ -1,6 +1,5 @@ // // ContentBlockerDebugEvents.swift -// DuckDuckGo // // Copyright © 2019 DuckDuckGo. All rights reserved. // @@ -25,17 +24,17 @@ public enum ContentBlockerDebugEvents { static let etag = "etag" static let errorDescription = "error_desc" } - + public enum Component: String, CustomStringConvertible, CaseIterable { - + public var description: String { rawValue } - + case tds case allowlist case tempUnprotected case localUnprotected case fallbackTds - + } case trackerDataParseFailed @@ -44,7 +43,7 @@ public enum ContentBlockerDebugEvents { case privacyConfigurationReloadFailed case privacyConfigurationParseFailed case privacyConfigurationCouldNotBeLoaded - + case contentBlockingCompilationFailed(listName: String, component: Component) case contentBlockingCompilationTime diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesIdentifier.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesIdentifier.swift index 1c5757e79..245fcbac2 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesIdentifier.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesIdentifier.swift @@ -1,6 +1,5 @@ // // ContentBlockerRulesIdentifier.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -20,68 +19,68 @@ import Foundation public class ContentBlockerRulesIdentifier: Equatable, Codable { - + let name: String let tdsEtag: String let tempListId: String let allowListId: String let unprotectedSitesHash: String - + public var stringValue: String { return name + tdsEtag + tempListId + allowListId + unprotectedSitesHash } - + public struct Difference: OptionSet { public let rawValue: Int - + public init(rawValue: Int) { self.rawValue = rawValue } - + public static let tdsEtag = Difference(rawValue: 1 << 0) public static let tempListId = Difference(rawValue: 1 << 1) public static let allowListId = Difference(rawValue: 1 << 2) public static let unprotectedSites = Difference(rawValue: 1 << 3) - + public static let all: Difference = [.tdsEtag, .tempListId, .allowListId, .unprotectedSites] } - + private class func normalize(identifier: String?) -> String { // Ensure identifier is in double quotes guard var identifier = identifier else { return "\"\"" } - + if !identifier.hasSuffix("\"") { identifier += "\"" } - + if !identifier.hasPrefix("\"") || identifier.count == 1 { identifier = "\"" + identifier } - + return identifier } - + public class func hash(domains: [String]?) -> String { guard let domains = domains, !domains.isEmpty else { return "" } - + return domains.joined().sha1 } - + public init(name: String, tdsEtag: String, tempListId: String?, allowListId: String?, unprotectedSitesHash: String?) { - + self.name = Self.normalize(identifier: name) self.tdsEtag = Self.normalize(identifier: tdsEtag) self.tempListId = Self.normalize(identifier: tempListId) self.allowListId = Self.normalize(identifier: allowListId) self.unprotectedSitesHash = Self.normalize(identifier: unprotectedSitesHash) } - + public func compare(with id: ContentBlockerRulesIdentifier) -> Difference { - + var result = Difference() if tdsEtag != id.tdsEtag { result.insert(.tdsEtag) @@ -95,7 +94,7 @@ public class ContentBlockerRulesIdentifier: Equatable, Codable { if unprotectedSitesHash != id.unprotectedSitesHash { result.insert(.unprotectedSites) } - + return result } diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesManager.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesManager.swift index 52c02d635..b980a60d9 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesManager.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesManager.swift @@ -1,6 +1,5 @@ // // ContentBlockerRulesManager.swift -// DuckDuckGo // // Copyright © 2020 DuckDuckGo. All rights reserved. // @@ -23,16 +22,14 @@ import TrackerRadarKit import Combine import Common -// swiftlint:disable file_length type_body_length - public protocol CompiledRuleListsSource { - + // Represent set of all latest rules that has been compiled var currentRules: [ContentBlockerRulesManager.Rules] { get } - + // Set of core rules: TDS minus Ad Attribution rules var currentMainRules: ContentBlockerRulesManager.Rules? { get } - + // Rules related to Ad Attribution feature, extracted from TDS set. var currentAttributionRules: ContentBlockerRulesManager.Rules? { get } } @@ -80,7 +77,7 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { self.etag = etag self.identifier = identifier } - + internal init(compilationResult: (compiledRulesList: WKContentRuleList, model: ContentBlockerRulesSourceModel)) { let surrogateTDS = ContentBlockerRulesManager.extractSurrogates(from: compilationResult.model.tds) let encodedData = try? JSONEncoder().encode(surrogateTDS) @@ -131,7 +128,7 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { private var compilationStartTime: TimeInterval? private let workQueue = DispatchQueue(label: "ContentBlockerManagerQueue", qos: .userInitiated) - + private let lastCompiledRulesStore: LastCompiledRulesStore? public init(rulesSource: ContentBlockerRulesListsSource, @@ -159,7 +156,7 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { } } } - + /** Variables protected by this lock: - state @@ -181,11 +178,11 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { lock.unlock() } } - + public var currentMainRules: Rules? { currentRules.first(where: { $0.name == DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName }) } - + public var currentAttributionRules: Rules? { currentRules.first(where: { let tdsName = DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName @@ -295,7 +292,7 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { } } } - + private func startCompilationProcess() { prepareSourceManagers() @@ -380,10 +377,10 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { lock.unlock() } - + private func applyRules(_ rules: [Rules], changes: [String: ContentBlockerRulesIdentifier.Difference] = [:]) { lock.lock() - + _currentRules = rules let completionTokens: [CompletionToken] @@ -408,9 +405,9 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { assertionFailure("Unexpected state") completionTokens = [] } - + lock.unlock() - + let currentIdentifiers: [String] = rules.map { $0.identifier.stringValue } updatesSubject.send(UpdateEvent(rules: rules, changes: changes, completionTokens: completionTokens)) @@ -449,5 +446,3 @@ public class ContentBlockerRulesManager: CompiledRuleListsSource { } } - -// swiftlint:enable file_length type_body_length diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSource.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSource.swift index e3c9a37d0..a981f7ee4 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSource.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSource.swift @@ -1,6 +1,5 @@ // // ContentBlockerRulesSource.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -56,7 +55,7 @@ public class ContentBlockerRulesList { }() public let name: String - + public init(name: String, trackerData: @escaping @autoclosure () -> TrackerDataManager.DataSet?, fallbackTrackerData: @escaping @autoclosure () -> TrackerDataManager.DataSet) { @@ -67,7 +66,7 @@ public class ContentBlockerRulesList { } open class DefaultContentBlockerRulesListsSource: ContentBlockerRulesListsSource { - + public struct Constants { public static let trackerDataSetRulesListName = "TrackerDataSet" } @@ -77,7 +76,7 @@ open class DefaultContentBlockerRulesListsSource: ContentBlockerRulesListsSource public init(trackerDataManager: TrackerDataManager) { self.trackerDataManager = trackerDataManager } - + open var contentBlockerRulesLists: [ContentBlockerRulesList] { return [ContentBlockerRulesList(name: Constants.trackerDataSetRulesListName, trackerData: self.trackerDataManager.fetchedData, diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSourceManager.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSourceManager.swift index b67712c6f..16f8305e0 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSourceManager.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockerRulesSourceManager.swift @@ -1,6 +1,5 @@ // // ContentBlockerRulesSourceManager.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -73,7 +72,7 @@ public class ContentBlockerRulesSourceModel: ContentBlockerRulesSourceIdentifier Manages sources that are used to compile Content Blocking Rules, handles possible broken state by filtering out sources that are potentially corrupted. */ public class ContentBlockerRulesSourceManager { - + public class RulesSourceBreakageInfo { public internal(set) var tdsIdentifier: String? @@ -177,7 +176,7 @@ public class ContentBlockerRulesSourceManager { return result } - + /** Process information about last failed compilation in order to update `brokenSources` state. */ @@ -199,7 +198,7 @@ public class ContentBlockerRulesSourceManager { private func compilationFailed(for input: ContentBlockerRulesSourceIdentifiers, with error: Error, brokenSources: RulesSourceBreakageInfo) { - + if input.tdsIdentifier != rulesList.fallbackTrackerData.etag { os_log(.debug, log: log, "Falling back to embedded TDS") // We failed compilation for non-embedded TDS, marking it as broken. @@ -261,5 +260,5 @@ public class ContentBlockerRulesSourceManager { fatalError("Could not compile embedded rules list") } } - + } diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesCompilationTask.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesCompilationTask.swift index 638f7be9f..74cf980ae 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesCompilationTask.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesCompilationTask.swift @@ -1,6 +1,5 @@ // // ContentBlockingRulesCompilationTask.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesLastCompiledRulesLookupTask.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesLastCompiledRulesLookupTask.swift index a7a06996a..021e058ed 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesLastCompiledRulesLookupTask.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesLastCompiledRulesLookupTask.swift @@ -1,6 +1,5 @@ // // ContentBlockingRulesLastCompiledRulesLookupTask.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,16 +21,16 @@ import WebKit import TrackerRadarKit extension ContentBlockerRulesManager { - + final class LastCompiledRulesLookupTask { - + struct CachedRulesList { let name: String let rulesList: WKContentRuleList let tds: TrackerData let rulesIdentifier: ContentBlockerRulesIdentifier } - + private let sourceRules: [ContentBlockerRulesList] private let lastCompiledRules: [LastCompiledRules] diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesLookupTask.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesLookupTask.swift index 0f1396267..974c984ac 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesLookupTask.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesLookupTask.swift @@ -1,6 +1,5 @@ // // ContentBlockingRulesLookupTask.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/ContentBlocking/DomainsProtectionStore.swift b/Sources/BrowserServicesKit/ContentBlocking/DomainsProtectionStore.swift index fd0bfc13f..db15bf545 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/DomainsProtectionStore.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/DomainsProtectionStore.swift @@ -1,6 +1,5 @@ // // DomainsProtectionStore.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/ContentBlocking/FailedCompilationsStore.swift b/Sources/BrowserServicesKit/ContentBlocking/FailedCompilationsStore.swift index f41a2a932..ea97f1385 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/FailedCompilationsStore.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/FailedCompilationsStore.swift @@ -1,6 +1,5 @@ // // FailedCompilationsStore.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/ContentBlocking/LastCompiledRulesStore.swift b/Sources/BrowserServicesKit/ContentBlocking/LastCompiledRulesStore.swift index 9d180b340..6c576f6dc 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/LastCompiledRulesStore.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/LastCompiledRulesStore.swift @@ -1,6 +1,5 @@ // // LastCompiledRulesStore.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -21,17 +20,17 @@ import Foundation import TrackerRadarKit public protocol LastCompiledRules { - + var name: String { get } var trackerData: TrackerData { get } var etag: String { get } var identifier: ContentBlockerRulesIdentifier { get } - + } public protocol LastCompiledRulesStore { - + var rules: [LastCompiledRules] { get } func update(with contentBlockerRules: [ContentBlockerRulesManager.Rules]) - + } diff --git a/Sources/BrowserServicesKit/ContentBlocking/TrackerDataManager.swift b/Sources/BrowserServicesKit/ContentBlocking/TrackerDataManager.swift index 45c6aaaa5..b98719a8d 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/TrackerDataManager.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/TrackerDataManager.swift @@ -1,6 +1,5 @@ // // TrackerDataManager.swift -// DuckDuckGo // // Copyright © 2019 DuckDuckGo. All rights reserved. // @@ -29,17 +28,17 @@ public protocol TrackerDataProvider { } public class TrackerDataManager { - + public enum ReloadResult: Equatable { case embedded case embeddedFallback case downloaded } - + public typealias DataSet = (tds: TrackerData, etag: String) - + private let lock = NSLock() - + private var _fetchedData: DataSet? private(set) public var fetchedData: DataSet? { get { @@ -54,7 +53,7 @@ public class TrackerDataManager { lock.unlock() } } - + private var _embeddedData: DataSet! private(set) public var embeddedData: DataSet { get { @@ -78,7 +77,7 @@ public class TrackerDataManager { lock.unlock() } } - + public var trackerData: TrackerData { if let data = fetchedData { return data.tds @@ -101,13 +100,13 @@ public class TrackerDataManager { @discardableResult public func reload(etag: String?, data: Data?) -> ReloadResult { - + let result: ReloadResult - + if let etag = etag, let data = data { result = .downloaded - + do { // This might fail if the downloaded data is corrupt or format has changed unexpectedly let data = try JSONDecoder().decode(TrackerData.self, from: data) @@ -121,7 +120,7 @@ public class TrackerDataManager { fetchedData = nil result = .embedded } - + return result } } diff --git a/Sources/BrowserServicesKit/ContentBlocking/TrackerDataQueryExtension.swift b/Sources/BrowserServicesKit/ContentBlocking/TrackerDataQueryExtension.swift index 00b211c48..4959160d8 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/TrackerDataQueryExtension.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/TrackerDataQueryExtension.swift @@ -1,6 +1,5 @@ // // TrackerDataQueryExtension.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -21,11 +20,11 @@ import Foundation import TrackerRadarKit extension TrackerData { - + public func findEntity(byName name: String) -> Entity? { return entities[name] } - + public func findEntity(forHost host: String) -> Entity? { for host in variations(of: host) { if let entityName = domains[host] { @@ -45,23 +44,23 @@ extension TrackerData { } return domains } - + public func findTracker(forUrl url: String) -> KnownTracker? { guard let host = URL(string: url)?.host else { return nil } - + let variations = variations(of: host) for host in variations { if let tracker = trackers[host] { return tracker } } - + return nil } - + public func findTrackerByCname(forUrl url: String) -> KnownTracker? { guard let host = URL(string: url)?.host else { return nil } - + let variations = variations(of: host) for host in variations { if let cname = cnames?[host] { @@ -70,7 +69,7 @@ extension TrackerData { return tracker } } - + return nil } } diff --git a/Sources/BrowserServicesKit/ContentBlocking/UserScripts/ContentBlockerRulesUserScript.swift b/Sources/BrowserServicesKit/ContentBlocking/UserScripts/ContentBlockerRulesUserScript.swift index b7d4703d5..108ca597d 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/UserScripts/ContentBlockerRulesUserScript.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/UserScripts/ContentBlockerRulesUserScript.swift @@ -1,6 +1,5 @@ // // ContentBlockerRulesUserScript.swift -// DuckDuckGo // // Copyright © 2020 DuckDuckGo. All rights reserved. // @@ -56,7 +55,7 @@ public class DefaultContentBlockerUserScriptConfig: ContentBlockerUserScriptConf ctlTrackerData: TrackerData?, tld: TLD, trackerDataManager: TrackerDataManager? = nil) { - + if trackerData == nil { // Fallback to embedded self.trackerData = trackerDataManager?.trackerData @@ -74,7 +73,7 @@ public class DefaultContentBlockerUserScriptConfig: ContentBlockerUserScriptConf } open class ContentBlockerRulesUserScript: NSObject, UserScript { - + struct ContentBlockerKey { static let url = "url" static let resourceType = "resourceType" @@ -89,20 +88,20 @@ open class ContentBlockerRulesUserScript: NSObject, UserScript { super.init() } - + public var source: String { return configuration.source } public var injectionTime: WKUserScriptInjectionTime = .atDocumentStart - + public var forMainFrameOnly: Bool = false - + public var messageNames: [String] = [ "processRule" ] - + public var supplementaryTrackerData = [TrackerData]() public var currentAdClickAttributionVendor: String? - + public weak var delegate: ContentBlockerRulesUserScriptDelegate? private var _temporaryUnprotectedDomainsCache = [String: [String]]() @@ -124,36 +123,36 @@ open class ContentBlockerRulesUserScript: NSObject, UserScript { guard let delegate = delegate else { return } guard delegate.contentBlockerRulesUserScriptShouldProcessTrackers(self) else { return } let ctlEnabled = delegate.contentBlockerRulesUserScriptShouldProcessCTLTrackers(self) - + guard let dict = message.body as? [String: Any] else { return } - + // False if domain is in unprotected list guard let blocked = dict[ContentBlockerKey.blocked] as? Bool else { return } guard let trackerUrlString = dict[ContentBlockerKey.url] as? String else { return } let resourceType = (dict[ContentBlockerKey.resourceType] as? String) ?? "unknown" guard let pageUrlStr = dict[ContentBlockerKey.pageUrl] as? String else { return } - + guard let currentTrackerData = configuration.trackerData else { return } let privacyConfiguration = configuration.privacyConfiguration - + var additionalTDSSets = supplementaryTrackerData - + if ctlEnabled, let ctlTrackerData = configuration.ctlTrackerData { additionalTDSSets.append(ctlTrackerData) } - + var detectedTracker: DetectedRequest? - + for trackerData in additionalTDSSets { let resolver = TrackerResolver(tds: trackerData, unprotectedSites: privacyConfiguration.userUnprotectedDomains, tempList: temporaryUnprotectedDomains, tld: configuration.tld, adClickAttributionVendor: currentAdClickAttributionVendor) - + if let tracker = resolver.trackerFromUrl(trackerUrlString, pageUrlString: pageUrlStr, resourceType: resourceType, @@ -172,21 +171,21 @@ open class ContentBlockerRulesUserScript: NSObject, UserScript { unprotectedSites: privacyConfiguration.userUnprotectedDomains, tempList: temporaryUnprotectedDomains, tld: configuration.tld) - + if let tracker = resolver.trackerFromUrl(trackerUrlString, pageUrlString: pageUrlStr, resourceType: resourceType, potentiallyBlocked: blocked && privacyConfiguration.isEnabled(featureKey: .contentBlocking)) { detectedTracker = tracker } - + if let tracker = detectedTracker { guard !isFirstParty(requestURL: tracker.url, websiteURL: pageUrlStr) else { return } delegate.contentBlockerRulesUserScript(self, detectedTracker: tracker) } else { guard let requestETLDp1 = configuration.tld.eTLDplus1(forStringURL: trackerUrlString), !isFirstParty(requestURL: trackerUrlString, websiteURL: pageUrlStr) else { return } - + let entity = currentTrackerData.findEntity(forHost: requestETLDp1) ?? Entity(displayName: requestETLDp1, domains: nil, prevalence: nil) let thirdPartyRequest = DetectedRequest(url: trackerUrlString, eTLDplus1: requestETLDp1, @@ -197,12 +196,12 @@ open class ContentBlockerRulesUserScript: NSObject, UserScript { delegate.contentBlockerRulesUserScript(self, detectedThirdPartyRequest: thirdPartyRequest) } } - + private func isFirstParty(requestURL: String, websiteURL: String) -> Bool { guard let requestDomain = configuration.tld.eTLDplus1(forStringURL: requestURL), let websiteDomain = configuration.tld.eTLDplus1(forStringURL: websiteURL) else { return false } - + return requestDomain == websiteDomain } diff --git a/Sources/BrowserServicesKit/ContentBlocking/UserScripts/SurrogatesUserScript.swift b/Sources/BrowserServicesKit/ContentBlocking/UserScripts/SurrogatesUserScript.swift index 8d61331dd..ba2b69cfb 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/UserScripts/SurrogatesUserScript.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/UserScripts/SurrogatesUserScript.swift @@ -1,6 +1,5 @@ // // SurrogatesUserScript.swift -// Core // // Copyright © 2020 DuckDuckGo. All rights reserved. // @@ -24,7 +23,7 @@ import ContentBlocking import Common public protocol SurrogatesUserScriptDelegate: NSObjectProtocol { - + func surrogatesUserScriptShouldProcessTrackers(_ script: SurrogatesUserScript) -> Bool func surrogatesUserScript(_ script: SurrogatesUserScript, detectedTracker tracker: DetectedRequest, @@ -51,7 +50,7 @@ public class DefaultSurrogatesUserScriptConfig: SurrogatesUserScriptConfig { public let tld: TLD public let source: String - + public init(privacyConfig: PrivacyConfiguration, surrogates: String, trackerData: TrackerData?, diff --git a/Sources/BrowserServicesKit/ContentBlocking/UserScripts/TrackerResolver.swift b/Sources/BrowserServicesKit/ContentBlocking/UserScripts/TrackerResolver.swift index f2fa6ace2..7e1d98d51 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/UserScripts/TrackerResolver.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/UserScripts/TrackerResolver.swift @@ -1,6 +1,5 @@ // // TrackerResolver.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -23,13 +22,13 @@ import Common import ContentBlocking public class TrackerResolver { - + let tds: TrackerData let unprotectedSites: [String] let tempList: [String] let tld: TLD let adClickAttributionVendor: String? - + public init(tds: TrackerData, unprotectedSites: [String], tempList: [String], @@ -41,7 +40,7 @@ public class TrackerResolver { self.tld = tld self.adClickAttributionVendor = adClickAttributionVendor } - + public func trackerFromUrl(_ trackerUrlString: String, pageUrlString: String, resourceType: String, @@ -58,11 +57,11 @@ public class TrackerResolver { } else { return nil } - + guard let entity = tds.findEntity(byName: tracker.owner?.name ?? "") else { return nil } - + if isPageAffiliatedWithTrackerEntity(pageUrlString: pageUrlString, trackerEntity: entity) { return DetectedRequest(url: trackerUrlString, eTLDplus1: tld.eTLDplus1(forStringURL: trackerUrlString), @@ -71,7 +70,7 @@ public class TrackerResolver { state: .allowed(reason: .ownedByFirstParty), pageUrl: pageUrlString) } - + let blockingState = calculateBlockingState(tracker: tracker, trackerUrlString: trackerUrlString, resourceType: resourceType, @@ -85,15 +84,15 @@ public class TrackerResolver { state: blockingState, pageUrl: pageUrlString) } - + private func isPageAffiliatedWithTrackerEntity(pageUrlString: String, trackerEntity: Entity) -> Bool { guard let pageHost = URL(string: pageUrlString)?.host, let pageEntity = tds.findEntity(forHost: pageHost) else { return false } - + return pageEntity.displayName == trackerEntity.displayName } - + private func calculateBlockingState(tracker: KnownTracker, trackerUrlString: String, resourceType: String, @@ -101,7 +100,7 @@ public class TrackerResolver { pageUrlString: String) -> BlockingState { let blockingState: BlockingState - + if isPageOnUnprotectedSitesOrTempList(pageUrlString) { blockingState = .allowed(reason: .protectionDisabled) // maybe we should not differentiate } else { @@ -130,7 +129,7 @@ public class TrackerResolver { blockingState = potentiallyBlocked ? .blocked : .allowed(reason: .ruleException) } } - + return blockingState } @@ -150,20 +149,20 @@ public class TrackerResolver { } return nil } - + private func isPageOnUnprotectedSitesOrTempList(_ pageUrlString: String) -> Bool { guard let pageHost = URL(string: pageUrlString)?.host else { return false } - + return unprotectedSites.contains(pageHost) || tempList.contains(pageHost) } - + private func isVendorMatchingCurrentPage(vendor: String, pageUrlString: String) -> Bool { vendor == URL(string: pageUrlString)?.host?.droppingWwwPrefix() } - + private func isVendorOnExceptionsList(vendor: String, exceptions: KnownTracker.Rule.Matching?) -> Bool { guard let domains = exceptions?.domains else { return false } - + return domains.contains(vendor) } @@ -203,7 +202,7 @@ fileprivate extension KnownTracker.Rule { guard let pattern = rule, let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return false } return regex.firstMatch(in: urlString, options: [], range: urlString.fullRange) != nil } - + func action(type: String, host: String) -> TrackerResolver.RuleAction? { // If there is a rule its default action is always block var resultAction: KnownTracker.ActionType? = action ?? .block @@ -220,7 +219,7 @@ fileprivate extension KnownTracker.Rule { } private extension KnownTracker.ActionType { - + func toTrackerResolverRuleAction() -> TrackerResolver.RuleAction { self == .block ? .blockRequest : .allowRequest } diff --git a/Sources/BrowserServicesKit/ContentScopeScript/ContentScopeUserScript.swift b/Sources/BrowserServicesKit/ContentScopeScript/ContentScopeUserScript.swift index 920b069b7..ad7c306d7 100644 --- a/Sources/BrowserServicesKit/ContentScopeScript/ContentScopeUserScript.swift +++ b/Sources/BrowserServicesKit/ContentScopeScript/ContentScopeUserScript.swift @@ -22,7 +22,6 @@ import Combine import ContentScopeScripts import UserScript import Common -import os.log public final class ContentScopeProperties: Encodable { public let globalPrivacyControlValue: Bool diff --git a/Sources/BrowserServicesKit/ContentScopeScript/SpecialPagesUserScript.swift b/Sources/BrowserServicesKit/ContentScopeScript/SpecialPagesUserScript.swift index b03943305..e33ea8c51 100644 --- a/Sources/BrowserServicesKit/ContentScopeScript/SpecialPagesUserScript.swift +++ b/Sources/BrowserServicesKit/ContentScopeScript/SpecialPagesUserScript.swift @@ -22,7 +22,6 @@ import Combine import ContentScopeScripts import UserScript import Common -import os.log public final class SpecialPagesUserScript: NSObject, UserScript, UserScriptMessaging { public var source: String = "" diff --git a/Sources/BrowserServicesKit/Email/EmailKeychainManager.swift b/Sources/BrowserServicesKit/Email/EmailKeychainManager.swift index de11fa90a..7dc68f424 100644 --- a/Sources/BrowserServicesKit/Email/EmailKeychainManager.swift +++ b/Sources/BrowserServicesKit/Email/EmailKeychainManager.swift @@ -1,6 +1,5 @@ // -// EmailKeyChainManager.swift -// DuckDuckGo +// EmailKeychainManager.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -34,11 +33,11 @@ extension EmailKeychainManager: EmailManagerStorage { public func getUsername() throws -> String? { try EmailKeychainManager.getString(forField: .username) } - + public func getToken() throws -> String? { try EmailKeychainManager.getString(forField: .token) } - + public func getAlias() throws -> String? { try EmailKeychainManager.getString(forField: .alias) } @@ -50,11 +49,11 @@ extension EmailKeychainManager: EmailManagerStorage { public func getLastUseDate() throws -> String? { try EmailKeychainManager.getString(forField: .lastUseDate) } - + public func store(token: String, username: String, cohort: String?) throws { try EmailKeychainManager.add(token: token, forUsername: username, cohort: cohort) } - + public func store(alias: String) throws { try EmailKeychainManager.add(alias: alias) } @@ -62,11 +61,11 @@ extension EmailKeychainManager: EmailManagerStorage { public func store(lastUseDate: String) throws { try EmailKeychainManager.add(lastUseDate: lastUseDate) } - + public func deleteAlias() throws { try EmailKeychainManager.deleteItem(forField: .alias) } - + public func deleteAuthenticationState() throws { try EmailKeychainManager.deleteAuthenticationState() } @@ -92,24 +91,24 @@ private extension EmailKeychainManager { case waitlistTimestamp = ".email.waitlistTimestamp" case inviteCode = ".email.inviteCode" case cohort = ".email.cohort" - + var keyValue: String { (Bundle.main.bundleIdentifier ?? "com.duckduckgo") + rawValue } } - + static func getString(forField field: EmailKeychainField) throws -> String? { guard let data = try retrieveData(forField: field) else { return nil } - + if let decodedString = String(data: data, encoding: String.Encoding.utf8) { return decodedString } else { throw EmailKeychainAccessError.failedToDecodeKeychainDataAsString } } - + static func retrieveData(forField field: EmailKeychainField, useDataProtectionKeychain: Bool = true) throws -> Data? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, @@ -118,7 +117,7 @@ private extension EmailKeychainManager { kSecReturnData as String: true, kSecUseDataProtectionKeychain as String: useDataProtectionKeychain ] - + var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) @@ -134,7 +133,7 @@ private extension EmailKeychainManager { throw EmailKeychainAccessError.keychainLookupFailure(status) } } - + static func add(token: String, forUsername username: String, cohort: String?) throws { guard let tokenData = token.data(using: .utf8), let usernameData = username.data(using: .utf8) else { @@ -155,7 +154,7 @@ private extension EmailKeychainManager { try add(data: cohortData, forField: .cohort) } } - + static func add(alias: String) throws { try add(string: alias, forField: .alias) } @@ -168,11 +167,11 @@ private extension EmailKeychainManager { guard let stringData = string.data(using: .utf8) else { return } - + try deleteItem(forField: field) try add(data: stringData, forField: field) } - + static func add(data: Data, forField field: EmailKeychainField, useDataProtectionKeychain: Bool = true) throws { let query = [ kSecClass: kSecClassGenericPassword, @@ -181,14 +180,14 @@ private extension EmailKeychainManager { kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock, kSecValueData: data, kSecUseDataProtectionKeychain: useDataProtectionKeychain] as [String: Any] - + let status = SecItemAdd(query as CFDictionary, nil) - + if status != errSecSuccess { throw EmailKeychainAccessError.keychainSaveFailure(status) } } - + static func deleteAuthenticationState() throws { try deleteItem(forField: .username) try deleteItem(forField: .token) @@ -196,15 +195,15 @@ private extension EmailKeychainManager { try deleteItem(forField: .cohort) try deleteItem(forField: .lastUseDate) } - + static func deleteItem(forField field: EmailKeychainField, useDataProtectionKeychain: Bool = true) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: field.keyValue, kSecUseDataProtectionKeychain as String: useDataProtectionKeychain] - + let status = SecItemDelete(query as CFDictionary) - + if status != errSecSuccess && status != errSecItemNotFound { throw EmailKeychainAccessError.keychainDeleteFailure(status) } @@ -231,12 +230,12 @@ public extension EmailKeychainManager { // MARK: - Keychain Migration Extensions extension EmailKeychainManager { - + /// Takes data from the login keychain and moves it to the data protection keychain. /// Reference: https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain static func migrateItemsToDataProtectionKeychain() { #if os(macOS) - + for field in EmailKeychainField.allCases { if let data = try? retrieveData(forField: field, useDataProtectionKeychain: false) { try? add(data: data, forField: field, useDataProtectionKeychain: true) @@ -246,5 +245,5 @@ extension EmailKeychainManager { #endif } - + } diff --git a/Sources/BrowserServicesKit/Email/EmailManager.swift b/Sources/BrowserServicesKit/Email/EmailManager.swift index a5f88c9a9..91548f6b1 100644 --- a/Sources/BrowserServicesKit/Email/EmailManager.swift +++ b/Sources/BrowserServicesKit/Email/EmailManager.swift @@ -1,6 +1,5 @@ // // EmailManager.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -20,8 +19,6 @@ import Foundation import Common -// swiftlint:disable file_length - public enum EmailKeychainAccessType: String { case getUsername case getToken @@ -43,7 +40,7 @@ public enum EmailKeychainAccessError: Error, Equatable { case keychainDeleteFailure(OSStatus) case keychainLookupFailure(OSStatus) case keychainFailedToSaveUsernameAfterSavingToken(OSStatus) - + public var errorDescription: String { switch self { case .failedToDecodeKeychainValueAsData: return "failedToDecodeKeychainValueAsData" @@ -105,7 +102,7 @@ public protocol EmailManagerRequestDelegate: AnyObject { timeoutInterval: TimeInterval) async throws -> Data func emailManagerKeychainAccessFailed(_ emailManager: EmailManager, - accessType: EmailKeychainAccessType, + accessType: EmailKeychainAccessType, error: EmailKeychainAccessError) } @@ -137,7 +134,7 @@ public struct EmailUrls { var emailAliasAPI: URL { return URL(string: Url.emailAlias)! } - + public init() { } } @@ -156,19 +153,19 @@ public enum EmailAliasStatus { } public class EmailManager { - + public static let emailDomain = "duck.com" private static let inContextEmailSignupPromptDismissedPermanentlyAtKey = "Autofill.InContextEmailSignup.dismissed.permanently.at" private let storage: EmailManagerStorage public weak var aliasPermissionDelegate: EmailManagerAliasPermissionDelegate? public weak var requestDelegate: EmailManagerRequestDelegate? - + public enum NotificationParameter { public static let cohort = "cohort" public static let isForcedSignOut = "isForcedSignOut" } - + private lazy var emailUrls = EmailUrls() private lazy var aliasAPIURL = emailUrls.emailAliasAPI @@ -191,7 +188,7 @@ public class EmailManager { } else { assertionFailure("Expected EmailKeychainAccessFailure") } - + return nil } } @@ -210,7 +207,7 @@ public class EmailManager { } else { assertionFailure("Expected EmailKeychainAccessFailure") } - + return nil } } @@ -229,7 +226,7 @@ public class EmailManager { } else { assertionFailure("Expected EmailKeychainAccessFailure") } - + return nil } } @@ -248,7 +245,7 @@ public class EmailManager { } else { assertionFailure("Expected EmailKeychainAccessFailure") } - + return nil } } @@ -267,7 +264,7 @@ public class EmailManager { } else { assertionFailure("Expected EmailKeychainAccessFailure") } - + return "" } } @@ -279,7 +276,7 @@ public class EmailManager { } let dateString = dateFormatter.string(from: Date()) - + do { try storage.store(lastUseDate: dateString) } catch { @@ -294,7 +291,7 @@ public class EmailManager { public var isSignedIn: Bool { return token != nil && username != nil } - + public var userEmail: String? { guard let username = username else { return nil } return username + "@" + EmailManager.emailDomain @@ -309,14 +306,14 @@ public class EmailManager { UserDefaults().set(newValue, forKey: Self.inContextEmailSignupPromptDismissedPermanentlyAtKey) } } - + public init(storage: EmailManagerStorage = EmailKeychainManager()) { self.storage = storage dateFormatter.formatOptions = [.withFullDate, .withDashSeparatorInDate] dateFormatter.timeZone = TimeZone(identifier: "America/New_York") // Use ET time zone } - + public func signOut(isForced: Bool = false) throws { Self.lock.lock() defer { @@ -427,25 +424,25 @@ extension EmailManager: AutofillEmailDelegate { public func autofillUserScriptDidRequestSignOut(_: AutofillUserScript) { try? self.signOut() } - + public func autofillUserScript(_: AutofillUserScript, didRequestAliasAndRequiresUserPermission requiresUserPermission: Bool, shouldConsumeAliasIfProvided: Bool, completionHandler: @escaping AliasAutosaveCompletion) { - + getAliasIfNeeded { [weak self] newAlias, error in guard let newAlias = newAlias, error == nil, let self = self else { completionHandler(nil, false, error) return } - + if requiresUserPermission { guard let delegate = self.aliasPermissionDelegate else { assertionFailure("EmailUserScript requires permission to provide Alias") completionHandler(nil, false, .permissionDelegateNil) return } - + delegate.emailManager(self, didRequestPermissionToProvideAliasWithCompletion: { [weak self] permissionType, autosave in switch permissionType { case .user: @@ -472,11 +469,11 @@ extension EmailManager: AutofillEmailDelegate { } } } - + public func autofillUserScriptDidRequestRefreshAlias(_: AutofillUserScript) { self.consumeAliasAndReplace() } - + public func autofillUserScript(_: AutofillUserScript, didRequestStoreToken token: String, username: String, cohort: String?) { try? storeToken(token, username: username, cohort: cohort) } @@ -587,14 +584,14 @@ private extension EmailManager { } typealias HTTPHeaders = [String: String] - + var emailHeaders: HTTPHeaders { guard let token = token else { return [:] } return ["Authorization": "Bearer " + token] } - + func consumeAliasAndReplace() { Self.lock.lock() defer { @@ -613,7 +610,7 @@ private extension EmailManager { fetchAndStoreAlias() } - + func getAliasIfNeeded(timeoutInterval: TimeInterval = 4.0, completionHandler: @escaping AliasCompletion) { if let alias = alias { completionHandler(alias, nil) @@ -666,7 +663,7 @@ private extension EmailManager { completionHandler?(nil, .signedOut) return } - + Task.detached { [aliasAPIURL, emailHeaders] in let result: Result do { @@ -768,5 +765,3 @@ private extension EmailManager { } } - -// swiftlint:enable file_length diff --git a/Sources/BrowserServicesKit/FeatureFlagger/FeatureFlagger.swift b/Sources/BrowserServicesKit/FeatureFlagger/FeatureFlagger.swift index 23abd039a..3e5f87f57 100644 --- a/Sources/BrowserServicesKit/FeatureFlagger/FeatureFlagger.swift +++ b/Sources/BrowserServicesKit/FeatureFlagger/FeatureFlagger.swift @@ -1,6 +1,5 @@ // // FeatureFlagger.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/GPC/GPCRequestFactory.swift b/Sources/BrowserServicesKit/GPC/GPCRequestFactory.swift index 610a22adc..11df31432 100644 --- a/Sources/BrowserServicesKit/GPC/GPCRequestFactory.swift +++ b/Sources/BrowserServicesKit/GPC/GPCRequestFactory.swift @@ -1,6 +1,5 @@ // // GPCRequestFactory.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -20,9 +19,9 @@ import Foundation public struct GPCRequestFactory { - + public init() { } - + public struct Constants { public static let secGPCHeader = "Sec-GPC" } @@ -50,21 +49,21 @@ public struct GPCRequestFactory { return false } - + public func requestForGPC(basedOn incomingRequest: URLRequest, config: PrivacyConfiguration, gpcEnabled: Bool) -> URLRequest? { - + func removingHeader(fromRequest incomingRequest: URLRequest) -> URLRequest? { var request = incomingRequest if let headers = request.allHTTPHeaderFields, headers.firstIndex(where: { $0.key == Constants.secGPCHeader }) != nil { request.setValue(nil, forHTTPHeaderField: Constants.secGPCHeader) return request } - + return nil } - + /* For now, the GPC header is only applied to sites known to be honoring GPC (nytimes.com, washingtonpost.com), while the DOM signal is available to all websites. @@ -74,7 +73,7 @@ public struct GPCRequestFactory { // Remove GPC header if its still there (or nil) return removingHeader(fromRequest: incomingRequest) } - + // Add GPC header if needed if config.isEnabled(featureKey: .gpc) && gpcEnabled { var request = incomingRequest @@ -87,7 +86,7 @@ public struct GPCRequestFactory { // Check if GPC header is still there and remove it return removingHeader(fromRequest: incomingRequest) } - + return nil } } diff --git a/Sources/BrowserServicesKit/InternalUserDecider/InternalUserDecider.swift b/Sources/BrowserServicesKit/InternalUserDecider/InternalUserDecider.swift index 9accf7019..397bbd7f3 100644 --- a/Sources/BrowserServicesKit/InternalUserDecider/InternalUserDecider.swift +++ b/Sources/BrowserServicesKit/InternalUserDecider/InternalUserDecider.swift @@ -1,6 +1,5 @@ // // InternalUserDecider.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,10 +20,10 @@ import Foundation import Combine public protocol InternalUserDecider { - + var isInternalUser: Bool { get } var isInternalUserPublisher: AnyPublisher { get } - + @discardableResult func markUserAsInternalIfNeeded(forUrl url: URL?, response: HTTPURLResponse?) -> Bool } @@ -37,7 +36,7 @@ public class DefaultInternalUserDecider: InternalUserDecider { var store: InternalUserStoring private static let internalUserVerificationURLHost = "use-login.duckduckgo.com" private let isInternalUserSubject: CurrentValueSubject - + public init(store: InternalUserStoring) { self.store = store isInternalUserSubject = CurrentValueSubject(store.isInternalUser) @@ -62,20 +61,20 @@ public class DefaultInternalUserDecider: InternalUserDecider { if isInternalUser { // If we're already an internal user, we don't need to do anything return false } - + if shouldMarkUserAsInternal(forUrl: url, statusCode: response?.statusCode) { isInternalUser = true return true } return false } - + func shouldMarkUserAsInternal(forUrl url: URL?, statusCode: Int?) -> Bool { if let statusCode = statusCode, statusCode == 200, let url = url, url.host == DefaultInternalUserDecider.internalUserVerificationURLHost { - + return true } return false diff --git a/Sources/BrowserServicesKit/LinkProtection/AMPCanonicalExtractor.swift b/Sources/BrowserServicesKit/LinkProtection/AMPCanonicalExtractor.swift index 13f653b8c..dba49856e 100644 --- a/Sources/BrowserServicesKit/LinkProtection/AMPCanonicalExtractor.swift +++ b/Sources/BrowserServicesKit/LinkProtection/AMPCanonicalExtractor.swift @@ -1,6 +1,5 @@ // // AMPCanonicalExtractor.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -21,9 +20,9 @@ import Foundation import WebKit import Common -public class AMPCanonicalExtractor: NSObject { +public final class AMPCanonicalExtractor: NSObject { - class CompletionHandler { + final class CompletionHandler { private var completion: ((URL?) -> Void)? @@ -49,17 +48,17 @@ public class AMPCanonicalExtractor: NSObject { } private let completionHandler = CompletionHandler() - + private var webView: WKWebView? private var imageBlockingRules: WKContentRuleList? - + private let linkCleaner: LinkCleaner private let privacyManager: PrivacyConfigurationManaging private let contentBlockingManager: CompiledRuleListsSource private let errorReporting: EventMapping? - + private var privacyConfig: PrivacyConfiguration { privacyManager.privacyConfig } - + public init(linkCleaner: LinkCleaner, privacyManager: PrivacyConfigurationManaging, contentBlockingManager: CompiledRuleListsSource, @@ -68,12 +67,12 @@ public class AMPCanonicalExtractor: NSObject { self.privacyManager = privacyManager self.contentBlockingManager = contentBlockingManager self.errorReporting = errorReporting - + super.init() - + loadImageBlockingRules() } - + private func loadImageBlockingRules() { WKContentRuleListStore.default().lookUpContentRuleList(forIdentifier: Constants.ruleListIdentifier) { [weak self] ruleList, _ in if let ruleList = ruleList { @@ -83,7 +82,7 @@ public class AMPCanonicalExtractor: NSObject { } } } - + private func compileImageRules() { let ruleSource = """ [ @@ -98,7 +97,7 @@ public class AMPCanonicalExtractor: NSObject { } ] """ - + WKContentRuleListStore.default().compileContentRuleList(forIdentifier: Constants.ruleListIdentifier, encodedContentRuleList: ruleSource) { [weak self] ruleList, error in if let error = error { @@ -106,24 +105,24 @@ public class AMPCanonicalExtractor: NSObject { self?.errorReporting?.fire(.ampBlockingRulesCompilationFailed) return } - + self?.imageBlockingRules = ruleList } } - + public func urlContainsAMPKeyword(_ url: URL?) -> Bool { linkCleaner.lastAMPURLString = nil guard privacyConfig.isEnabled(featureKey: .ampLinks) else { return false } guard let url = url, !linkCleaner.isURLExcluded(url: url) else { return false } let urlStr = url.absoluteString - + let ampKeywords = TrackingLinkSettings(fromConfig: privacyConfig).ampKeywords return ampKeywords.contains { keyword in return urlStr.contains(keyword) } } - + private func buildUserScript() -> WKUserScript { let source = """ (function() { @@ -135,10 +134,10 @@ public class AMPCanonicalExtractor: NSObject { }) })() """ - + return WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true) } - + public func cancelOngoingExtraction() { webView?.stopLoading() webView = nil @@ -162,7 +161,7 @@ public class AMPCanonicalExtractor: NSObject { completion(nil) return } - + completionHandler.setCompletionHandler(completion: completion) assert(Thread.isMainThread) @@ -170,7 +169,7 @@ public class AMPCanonicalExtractor: NSObject { webView?.navigationDelegate = self webView?.load(URLRequest(url: url)) } - + public func getCanonicalUrl(initiator: URL?, url: URL?) async -> URL? { await withCheckedContinuation { continuation in @@ -179,7 +178,7 @@ public class AMPCanonicalExtractor: NSObject { } } } - + private func makeConfiguration() -> WKWebViewConfiguration { let configuration = WKWebViewConfiguration() configuration.websiteDataStore = .nonPersistent() @@ -206,9 +205,9 @@ public class AMPCanonicalExtractor: NSObject { extension AMPCanonicalExtractor: WKScriptMessageHandler { public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == Constants.sendCanonical else { return } - + webView = nil - + if let dict = message.body as? [String: AnyObject], let canonical = dict[Constants.canonicalKey] as? String { if let canonicalUrl = URL(string: canonical), !linkCleaner.isURLExcluded(url: canonicalUrl) { @@ -227,7 +226,7 @@ extension AMPCanonicalExtractor: WKNavigationDelegate { public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { completionHandler.completeWithURL(nil) } - + public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { completionHandler.completeWithURL(nil) } diff --git a/Sources/BrowserServicesKit/LinkProtection/AMPProtectionDebugEvents.swift b/Sources/BrowserServicesKit/LinkProtection/AMPProtectionDebugEvents.swift index fad548a89..f1bfdbcfa 100644 --- a/Sources/BrowserServicesKit/LinkProtection/AMPProtectionDebugEvents.swift +++ b/Sources/BrowserServicesKit/LinkProtection/AMPProtectionDebugEvents.swift @@ -1,6 +1,5 @@ // -// ContentBlockerDebugEvents.swift -// DuckDuckGo +// AMPProtectionDebugEvents.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,5 +21,5 @@ import Foundation public enum AMPProtectionDebugEvents { case ampBlockingRulesCompilationFailed - + } diff --git a/Sources/BrowserServicesKit/LinkProtection/LinkCleaner.swift b/Sources/BrowserServicesKit/LinkProtection/LinkCleaner.swift index 3ff914a58..515675759 100644 --- a/Sources/BrowserServicesKit/LinkProtection/LinkCleaner.swift +++ b/Sources/BrowserServicesKit/LinkProtection/LinkCleaner.swift @@ -1,6 +1,5 @@ // // LinkCleaner.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -20,33 +19,33 @@ import Foundation public class LinkCleaner { - + public var lastAMPURLString: String? public var urlParametersRemoved: Bool = false - + private let privacyManager: PrivacyConfigurationManaging private var privacyConfig: PrivacyConfiguration { privacyManager.privacyConfig } public init(privacyManager: PrivacyConfigurationManaging) { self.privacyManager = privacyManager } - + public func ampFormat(matching url: URL) -> String? { let ampFormats = TrackingLinkSettings(fromConfig: privacyConfig).ampLinkFormats for format in ampFormats where url.absoluteString.matches(pattern: format) { return format } - + return nil } - + public func isURLExcluded(url: URL, feature: PrivacyFeature = .ampLinks) -> Bool { guard let host = url.host else { return true } guard url.scheme == "http" || url.scheme == "https" else { return true } - + return !privacyConfig.isFeature(feature, enabledForDomain: host) } - + public func extractCanonicalFromAMPLink(initiator: URL?, destination url: URL?) -> URL? { lastAMPURLString = nil guard privacyConfig.isEnabled(featureKey: .ampLinks) else { return nil } @@ -54,9 +53,9 @@ public class LinkCleaner { if let initiator = initiator, isURLExcluded(url: initiator) { return nil } - + guard let ampFormat = ampFormat(matching: url) else { return nil } - + do { let ampStr = url.absoluteString let regex = try NSRegularExpression(pattern: ampFormat, options: [.caseInsensitive]) @@ -65,14 +64,14 @@ public class LinkCleaner { range: NSRange(ampStr.startIndex.. URL? { urlParametersRemoved = false guard privacyConfig.isEnabled(featureKey: .trackingParameters) else { return url } @@ -92,25 +91,25 @@ public class LinkCleaner { if let initiator = initiator, isURLExcluded(url: initiator, feature: .trackingParameters) { return url } - + guard var urlsComps = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url } guard let queryParams = urlsComps.percentEncodedQueryItems, queryParams.count > 0 else { return url } - + let trackingParams = TrackingLinkSettings(fromConfig: privacyConfig).trackingParameters - + let preservedParams: [URLQueryItem] = queryParams.filter { param in if trackingParams.contains(where: { $0 == param.name }) { urlParametersRemoved = true return false } - + return true } - + if urlParametersRemoved { urlsComps.percentEncodedQueryItems = preservedParams.count > 0 ? preservedParams : nil return urlsComps.url diff --git a/Sources/BrowserServicesKit/LinkProtection/LinkProtection.swift b/Sources/BrowserServicesKit/LinkProtection/LinkProtection.swift index 7dd8b0986..907316b03 100644 --- a/Sources/BrowserServicesKit/LinkProtection/LinkProtection.swift +++ b/Sources/BrowserServicesKit/LinkProtection/LinkProtection.swift @@ -20,12 +20,12 @@ import WebKit import Common public struct LinkProtection { - + private let linkCleaner: LinkCleaner private let ampExtractor: AMPCanonicalExtractor - + private var mainFrameUrl: URL? - + public init(privacyManager: PrivacyConfigurationManaging, contentBlockingManager: CompiledRuleListsSource, errorReporting: EventMapping) { @@ -35,11 +35,11 @@ public struct LinkProtection { contentBlockingManager: contentBlockingManager, errorReporting: errorReporting) } - + public mutating func setMainFrameUrl(_ url: URL?) { mainFrameUrl = url } - + public func getCleanURL(from url: URL, onStartExtracting: () -> Void, onFinishExtracting: @escaping () -> Void, @@ -48,7 +48,7 @@ public struct LinkProtection { if let cleanURL = linkCleaner.cleanTrackingParameters(initiator: nil, url: urlToLoad) { urlToLoad = cleanURL } - + if let cleanURL = linkCleaner.extractCanonicalFromAMPLink(initiator: nil, destination: urlToLoad) { completion(cleanURL) } else if ampExtractor.urlContainsAMPKeyword(urlToLoad) { @@ -74,7 +74,7 @@ public struct LinkProtection { } } } - + // swiftlint:disable function_parameter_count public func requestTrackingLinkRewrite(initiatingURL: URL?, destinationURL: URL, @@ -87,7 +87,7 @@ public struct LinkProtection { // We do not rewrite redirects due to breakage concerns return false } - + var didRewriteLink = false if let newURL = linkCleaner.extractCanonicalFromAMPLink(initiator: initiatingURL, destination: destinationURL) { policyDecisionHandler(false) @@ -101,7 +101,7 @@ public struct LinkProtection { policyDecisionHandler(true) return } - + policyDecisionHandler(false) onLinkRewrite(canonical) } @@ -113,7 +113,7 @@ public struct LinkProtection { didRewriteLink = true } } - + return didRewriteLink } @@ -146,16 +146,16 @@ public struct LinkProtection { onLinkRewrite: onLinkRewrite) { navigationActionPolicy in continuation.resume(returning: navigationActionPolicy) } - + if !didRewriteLink { continuation.resume(returning: nil) } } } - + public func cancelOngoingExtraction() { ampExtractor.cancelOngoingExtraction() } - + public var lastAMPURLString: String? { linkCleaner.lastAMPURLString } public var urlParametersRemoved: Bool { linkCleaner.urlParametersRemoved } - + } diff --git a/Sources/BrowserServicesKit/LinkProtection/TrackingLinkSettings.swift b/Sources/BrowserServicesKit/LinkProtection/TrackingLinkSettings.swift index 30bcc43a5..f832567df 100644 --- a/Sources/BrowserServicesKit/LinkProtection/TrackingLinkSettings.swift +++ b/Sources/BrowserServicesKit/LinkProtection/TrackingLinkSettings.swift @@ -1,6 +1,5 @@ // // TrackingLinkSettings.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -20,24 +19,24 @@ import Foundation struct TrackingLinkSettings { - + let ampLinkFormats: [String] let ampKeywords: [String] let trackingParameters: [String] - + struct Constants { static let ampLinkFormats = "linkFormats" static let ampKeywords = "keywords" static let trackingParameters = "parameters" } - + init(fromConfig config: PrivacyConfiguration) { let ampFeatureSettings = config.settings(for: .ampLinks) let trackingParametersSettings = config.settings(for: .trackingParameters) - + ampLinkFormats = ampFeatureSettings[Constants.ampLinkFormats] as? [String] ?? [] ampKeywords = ampFeatureSettings[Constants.ampKeywords] as? [String] ?? [] trackingParameters = trackingParametersSettings[Constants.trackingParameters] as? [String] ?? [] } - + } diff --git a/Sources/BrowserServicesKit/PrivacyConfig/AppPrivacyConfiguration.swift b/Sources/BrowserServicesKit/PrivacyConfig/AppPrivacyConfiguration.swift index 3d5356c86..0922005cc 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/AppPrivacyConfiguration.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/AppPrivacyConfiguration.swift @@ -1,6 +1,5 @@ // // AppPrivacyConfiguration.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -21,7 +20,7 @@ import Foundation import Common public struct AppPrivacyConfiguration: PrivacyConfiguration { - + private enum Constants { static let enabledKey = "enabled" static let lastRolloutCountKey = "lastRolloutCount" @@ -29,7 +28,7 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { } private(set) public var identifier: String - + private let data: PrivacyConfigurationData private let locallyUnprotected: DomainsProtectionStore private let internalUserDecider: InternalUserDecider @@ -53,7 +52,7 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { public var userUnprotectedDomains: [String] { return Array(locallyUnprotected.unprotectedDomains).normalizedDomainsForContentBlocking().sorted() } - + public var tempUnprotectedDomains: [String] { return data.unprotectedTemporary.map { $0.domain }.normalizedDomainsForContentBlocking() } @@ -61,22 +60,22 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { public var trackerAllowlist: PrivacyConfigurationData.TrackerAllowlist { return data.trackerAllowlist } - + func parse(versionString: String) -> [Int] { return versionString.split(separator: ".").map { Int($0) ?? 0 } } - + func satisfiesMinVersion(_ version: String?, versionProvider: AppVersionProvider) -> Bool { if let minSupportedVersion = version, let appVersion = versionProvider.appVersion() { let minVersion = parse(versionString: minSupportedVersion) let currentVersion = parse(versionString: appVersion) - + for i in 0.. minSegment { return true } @@ -85,7 +84,7 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { } } } - + return true } @@ -115,25 +114,25 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { default: return false } } - + private func isRolloutEnabled(subfeature: any PrivacySubfeature, rolloutSteps: [PrivacyConfigurationData.PrivacyFeature.Feature.RolloutStep], randomizer: (Range) -> Double) -> Bool { // Empty rollouts should be default enabled guard !rolloutSteps.isEmpty else { return true } - + let defsPrefix = "config.\(subfeature.parent.rawValue).\(subfeature.rawValue)" if userDefaults.bool(forKey: "\(defsPrefix).\(Constants.enabledKey)") { return true } - + var willEnable = false let rollouts = Array(Set(rolloutSteps.filter({ $0.percent >= 0.0 && $0.percent <= 100.0 }))).sorted(by: { $0.percent < $1.percent }) if let rolloutSize = userDefaults.value(forKey: "\(defsPrefix).\(Constants.lastRolloutCountKey)") as? Int { guard rolloutSize < rollouts.count else { return false } // Sanity check as we need at least two values to compute the new probability guard rollouts.count > 1 else { return false } - + // If the user has seen the rollout before, and the rollout count has changed // Try again with the new probability let y = rollouts[rollouts.count - 1].percent @@ -153,7 +152,7 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { userDefaults.set(rollouts.count, forKey: "\(defsPrefix).\(Constants.lastRolloutCountKey)") return false } - + userDefaults.set(true, forKey: "\(defsPrefix).\(Constants.enabledKey)") return true } @@ -168,14 +167,14 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { let subfeatures = subfeatures(for: subfeature.parent) let subfeatureData = subfeatures[subfeature.rawValue] let satisfiesMinVersion = satisfiesMinVersion(subfeatureData?.minSupportedVersion, versionProvider: versionProvider) - + // Handle Rollouts if let rollout = subfeatureData?.rollout { if !isRolloutEnabled(subfeature: subfeature, rolloutSteps: rollout.steps, randomizer: randomizer) { return false } } - + switch subfeatureData?.state { case PrivacyConfigurationData.State.enabled: return satisfiesMinVersion case PrivacyConfigurationData.State.internal: return internalUserDecider.isInternalUser && satisfiesMinVersion @@ -186,10 +185,10 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { private func subfeatures(for feature: PrivacyFeature) -> PrivacyConfigurationData.PrivacyFeature.Features { return data.features[feature.rawValue]?.features ?? [:] } - + public func exceptionsList(forFeature featureKey: PrivacyFeature) -> [String] { guard let feature = data.features[featureKey.rawValue] else { return [] } - + return feature.exceptions.map { $0.domain }.normalizedDomainsForContentBlocking() } @@ -261,11 +260,11 @@ public struct AppPrivacyConfiguration: PrivacyConfiguration { public func userDisabledProtection(forDomain domain: String) { locallyUnprotected.disableProtection(forDomain: domain.punycodeEncodedHostname.lowercased()) } - + } extension Array where Element == String { - + func normalizedDomainsForContentBlocking() -> [String] { map { domain in domain.punycodeEncodedHostname.lowercased() diff --git a/Sources/BrowserServicesKit/PrivacyConfig/AppVersionProvider.swift b/Sources/BrowserServicesKit/PrivacyConfig/AppVersionProvider.swift index 045a08a61..cf497cde0 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/AppVersionProvider.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/AppVersionProvider.swift @@ -1,6 +1,5 @@ // // AppVersionProvider.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -21,7 +20,7 @@ import Foundation import Common open class AppVersionProvider { - + open func appVersion() -> String? { Bundle.main.releaseVersionNumber } public init() { } diff --git a/Sources/BrowserServicesKit/PrivacyConfig/Features/AdClickAttributionFeature.swift b/Sources/BrowserServicesKit/PrivacyConfig/Features/AdClickAttributionFeature.swift index 49dc45f9b..d335ef39d 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/Features/AdClickAttributionFeature.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/Features/AdClickAttributionFeature.swift @@ -1,6 +1,5 @@ // // AdClickAttributionFeature.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -21,42 +20,42 @@ import Foundation import Combine public protocol AdClickAttributing { - + var isEnabled: Bool { get } var allowlist: [AdClickAttributionFeature.AllowlistEntry] { get } var navigationExpiration: Double { get } var totalExpiration: Double { get } var isHeuristicDetectionEnabled: Bool { get } var isDomainDetectionEnabled: Bool { get } - + func isMatchingAttributionFormat(_ url: URL) -> Bool func attributionDomainParameterName(for: URL) -> String? } public class AdClickAttributionFeature: AdClickAttributing { - + private class LinkFormats { - + // Map host to related link formats private var linkFormats: [String: [LinkFormat]] - + init(linkFormatsJSON: [[String: String]]) { var linkFormatsMap = [String: [LinkFormat]]() for entry in linkFormatsJSON { guard let urlString = entry["url"], let url = URL(string: URL.URLProtocol.https.scheme + urlString), let host = url.host else { continue } - + let linkFormat = LinkFormat(url: url, adDomainParameterName: entry["adDomainParameterName"]) linkFormatsMap[host, default: []].append(linkFormat) } self.linkFormats = linkFormatsMap } - + func linkFormat(for url: URL) -> LinkFormat? { guard let domain = url.host else { return nil } - + for linkFormat in linkFormats[domain] ?? [] where linkFormat.url.host == domain && url.path == linkFormat.url.path { if let parameterMatching = linkFormat.adDomainParameterName, @@ -76,10 +75,10 @@ public class AdClickAttributionFeature: AdClickAttributing { static let heuristicDetectionKey = "heuristicDetection" static let domainDetectionKey = "domainDetection" } - + private let configManager: PrivacyConfigurationManaging var updateCancellable: AnyCancellable? - + public private(set) var isEnabled = false private var navigationLinkFormats = LinkFormats(linkFormatsJSON: []) public private(set) var allowlist = [AllowlistEntry]() @@ -87,18 +86,18 @@ public class AdClickAttributionFeature: AdClickAttributing { public private(set) var totalExpiration: Double = 0 public private(set) var isHeuristicDetectionEnabled: Bool = false public private(set) var isDomainDetectionEnabled: Bool = false - + public init(with manager: PrivacyConfigurationManaging) { - + configManager = manager - + updateCancellable = configManager.updatesPublisher.receive(on: DispatchQueue.main).sink { [weak self] in guard let strongSelf = self else { return } strongSelf.update(with: strongSelf.configManager.privacyConfig) } update(with: manager.privacyConfig) } - + public func update(with config: PrivacyConfiguration) { isEnabled = config.isEnabled(featureKey: .adClickAttribution) guard isEnabled else { @@ -109,12 +108,12 @@ public class AdClickAttributionFeature: AdClickAttributing { isDomainDetectionEnabled = false return } - + let settings = config.settings(for: .adClickAttribution) - + let linkFormats = settings[Constants.linkFormatsSettingsKey] as? [[String: String]] ?? [] navigationLinkFormats = LinkFormats(linkFormatsJSON: linkFormats) - + if let allowlist = settings[Constants.allowlistSettingsKey] as? [[String: String]] { self.allowlist = allowlist.compactMap({ entry in guard let host = entry["host"], let blocklistEntry = entry["blocklistEntry"] else { return nil } @@ -123,27 +122,27 @@ public class AdClickAttributionFeature: AdClickAttributing { } else { self.allowlist = [] } - + if let navigationExpiration = settings[Constants.navigationExpirationSettingsKey] as? NSNumber { self.navigationExpiration = navigationExpiration.doubleValue } else { navigationExpiration = 1800 } - + if let totalExpiration = settings[Constants.totalExpirationSettingsKey] as? NSNumber { self.totalExpiration = totalExpiration.doubleValue } else { totalExpiration = 604800 } - + isHeuristicDetectionEnabled = (settings[Constants.heuristicDetectionKey] as? String) == PrivacyConfigurationData.State.enabled isDomainDetectionEnabled = (settings[Constants.domainDetectionKey] as? String) == PrivacyConfigurationData.State.enabled } - + public func isMatchingAttributionFormat(_ url: URL) -> Bool { navigationLinkFormats.linkFormat(for: url) != nil } - + public func attributionDomainParameterName(for url: URL) -> String? { navigationLinkFormats.linkFormat(for: url)?.adDomainParameterName } @@ -151,13 +150,13 @@ public class AdClickAttributionFeature: AdClickAttributing { public struct AllowlistEntry { public let entity: String public let host: String - + public init(entity: String, host: String) { self.entity = entity self.host = host } } - + private struct LinkFormat { let url: URL let adDomainParameterName: String? diff --git a/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift b/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift index efab8d425..d8e85a604 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift @@ -1,6 +1,5 @@ // // PrivacyFeature.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfiguration.swift b/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfiguration.swift index 4be6d13ed..74f249289 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfiguration.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfiguration.swift @@ -1,6 +1,5 @@ // // PrivacyConfiguration.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationData.swift b/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationData.swift index ffe12ff63..7deba1283 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationData.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationData.swift @@ -1,6 +1,5 @@ // // PrivacyConfigurationData.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -19,7 +18,6 @@ import Foundation -// swiftlint:disable nesting public struct PrivacyConfigurationData { public typealias FeatureName = String @@ -140,12 +138,12 @@ public struct PrivacyConfigurationData { } public let percent: Double - + public init(json: [String: Any]) { self.percent = json[CodingKeys.percent.rawValue] as? Double ?? 0 } } - + public let state: FeatureState public let minSupportedVersion: FeatureSupportedVersion? public let rollout: Rollout? @@ -281,4 +279,3 @@ public struct PrivacyConfigurationData { } } } -// swiftlint:enable nesting diff --git a/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationManager.swift b/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationManager.swift index 66be2be0c..a6ec94c66 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationManager.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationManager.swift @@ -1,6 +1,5 @@ // // PrivacyConfigurationManager.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -37,7 +36,7 @@ public protocol PrivacyConfigurationManaging: AnyObject { } public class PrivacyConfigurationManager: PrivacyConfigurationManaging { - + public enum ReloadResult: Equatable { case embedded case embeddedFallback @@ -48,9 +47,8 @@ public class PrivacyConfigurationManager: PrivacyConfigurationManaging { case dataMismatch } - // swiftlint:disable:next large_tuple public typealias ConfigurationData = (rawData: Data, data: PrivacyConfigurationData, etag: String) - + private let lock = NSLock() private let embeddedDataProvider: EmbeddedDataProvider private let localProtection: DomainsProtectionStore @@ -62,7 +60,7 @@ public class PrivacyConfigurationManager: PrivacyConfigurationManaging { public var updatesPublisher: AnyPublisher { updatesSubject.eraseToAnyPublisher() } - + private var _fetchedConfigData: ConfigurationData? private(set) public var fetchedConfigData: ConfigurationData? { get { @@ -77,7 +75,7 @@ public class PrivacyConfigurationManager: PrivacyConfigurationManaging { lock.unlock() } } - + private var _embeddedConfigData: ConfigurationData! private(set) public var embeddedConfigData: ConfigurationData { get { @@ -120,7 +118,7 @@ public class PrivacyConfigurationManager: PrivacyConfigurationManaging { reload(etag: fetchedETag, data: fetchedData) } - + public var privacyConfig: PrivacyConfiguration { if let fetchedData = fetchedConfigData { return AppPrivacyConfiguration(data: fetchedData.data, @@ -136,7 +134,7 @@ public class PrivacyConfigurationManager: PrivacyConfigurationManaging { internalUserDecider: internalUserDecider, installDate: installDate) } - + public var currentConfig: Data { if let fetchedData = fetchedConfigData { return fetchedData.rawData @@ -146,14 +144,14 @@ public class PrivacyConfigurationManager: PrivacyConfigurationManaging { @discardableResult public func reload(etag: String?, data: Data?) -> ReloadResult { - + defer { self.updatesSubject.send() } - + let result: ReloadResult - + if let etag = etag, let data = data { result = .downloaded - + do { // This might fail if the downloaded data is corrupt or format has changed unexpectedly let configData = try PrivacyConfigurationData(data: data) @@ -167,7 +165,7 @@ public class PrivacyConfigurationManager: PrivacyConfigurationManaging { fetchedConfigData = nil result = .embedded } - + return result } } diff --git a/Sources/BrowserServicesKit/ReferrerTrimming/ReferrerTrimming.swift b/Sources/BrowserServicesKit/ReferrerTrimming/ReferrerTrimming.swift index 016037f48..8fdd57fe9 100644 --- a/Sources/BrowserServicesKit/ReferrerTrimming/ReferrerTrimming.swift +++ b/Sources/BrowserServicesKit/ReferrerTrimming/ReferrerTrimming.swift @@ -22,26 +22,26 @@ import TrackerRadarKit import Common public class ReferrerTrimming { - + struct Constants { static let headerName = "Referer" static let policyName = "Referrer-Policy" } - + public enum TrimmingState { case idle case navigating(destination: URL) } - + private let privacyManager: PrivacyConfigurationManaging private var privacyConfig: PrivacyConfiguration { privacyManager.privacyConfig } - + private let contentBlockingManager: CompiledRuleListsSource - + private var state: TrimmingState = .idle - + private var tld: TLD - + public init(privacyManager: PrivacyConfigurationManaging, contentBlockingManager: CompiledRuleListsSource, tld: TLD) { @@ -49,39 +49,39 @@ public class ReferrerTrimming { self.contentBlockingManager = contentBlockingManager self.tld = tld } - + public func onBeginNavigation(to destination: URL?) { guard let destination = destination else { return } - + state = .navigating(destination: destination) } - + public func onFinishNavigation() { state = .idle } - + public func onFailedNavigation() { state = .idle } - + func getTrimmedReferrer(originUrl: URL, destUrl: URL, referrerUrl: URL?, trackerData: TrackerData) -> String? { func isSameEntity(a: Entity?, b: Entity?) -> Bool { if a == nil && b == nil { return !originUrl.isThirdParty(to: destUrl, tld: tld) } - + return a?.displayName == b?.displayName } - + guard let originHost = originUrl.host else { return nil } guard let destHost = destUrl.host else { return nil } - + guard privacyConfig.isFeature(.referrer, enabledForDomain: originHost), privacyConfig.isFeature(.referrer, enabledForDomain: destHost) else { return nil @@ -91,10 +91,10 @@ public class ReferrerTrimming { let referrerHost = referrerUrl.host else { return nil } - + let referEntity = trackerData.findEntity(forHost: originHost) let destEntity = trackerData.findEntity(forHost: destHost) - + var newReferrer: String? if !isSameEntity(a: referEntity, b: destEntity) { newReferrer = "\(referrerScheme)://\(referrerHost)/" @@ -105,11 +105,11 @@ public class ReferrerTrimming { !isSameEntity(a: referEntity, b: destEntity) { newReferrer = "\(referrerScheme)://\(referrerHost)/" } - + if newReferrer == referrerUrl.absoluteString { return nil } - + return newReferrer } @@ -134,15 +134,15 @@ public class ReferrerTrimming { case .idle: onBeginNavigation(to: destUrl) } - + guard let trackerData = contentBlockingManager.currentMainRules?.trackerData else { return nil } - + guard let referrerHeader = request.value(forHTTPHeaderField: Constants.headerName) else { return nil } - + if let newReferrer = getTrimmedReferrer(originUrl: originUrl, destUrl: destUrl, referrerUrl: URL(string: referrerHeader) ?? nil, @@ -151,7 +151,7 @@ public class ReferrerTrimming { request.setValue(newReferrer, forHTTPHeaderField: Constants.headerName) return request } - + return nil } } diff --git a/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift b/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift index f4ff9d6ab..f4af0b3d2 100644 --- a/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift +++ b/Sources/BrowserServicesKit/SecureVault/AutofillDatabaseProvider.swift @@ -74,7 +74,6 @@ public protocol AutofillDatabaseProvider: SecureStorageDatabaseProvider { func updateSyncTimestamp(in database: Database, tableName: String, objectId: Int64, timestamp: Date?) throws } -// swiftlint:disable:next type_body_length public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabaseProvider, AutofillDatabaseProvider { public static func defaultDatabaseURL() -> URL { @@ -226,7 +225,7 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro do { var account = credentials.account account.title = account.patternMatchedTitle() - + try account.update(database) try database.execute(sql: """ UPDATE @@ -258,7 +257,7 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro do { var account = credentials.account account.title = account.patternMatchedTitle() - + try account.insert(database) let id = database.lastInsertedRowID try database.execute(sql: """ @@ -765,9 +764,9 @@ extension DefaultAutofillDatabaseProvider { let oldTableName = SecureVaultModels.CreditCard.databaseTableName + "Old" try database.rename(table: SecureVaultModels.CreditCard.databaseTableName, to: oldTableName) - + // 2. Create the new table with suffix and card data values: - + try database.create(table: SecureVaultModels.CreditCard.databaseTableName) { $0.autoIncrementedPrimaryKey(SecureVaultModels.CreditCard.Columns.id.name) @@ -782,13 +781,13 @@ extension DefaultAutofillDatabaseProvider { $0.column(SecureVaultModels.CreditCard.Columns.expirationMonth.name, .integer) $0.column(SecureVaultModels.CreditCard.Columns.expirationYear.name, .integer) } - + // 3. Iterate over existing records - read their numbers, store the suffixes, and then update the new table: - + let rows = try Row.fetchCursor(database, sql: "SELECT * FROM \(oldTableName)") while let row = try rows.next() { - + // Generate the encrypted card number and plaintext suffix: let number: String = row[SecureVaultModels.CreditCard.DeprecatedColumns.cardNumber.name] @@ -796,9 +795,9 @@ extension DefaultAutofillDatabaseProvider { let encryptedCardNumber = try MigrationUtility.l2encrypt(data: number.data(using: .utf8)!, cryptoProvider: cryptoProvider, keyStoreProvider: keyStoreProvider) - + // Insert data from the old table into the new one: - + try database.execute(sql: """ INSERT INTO \(SecureVaultModels.CreditCard.databaseTableName) @@ -831,19 +830,19 @@ extension DefaultAutofillDatabaseProvider { row[SecureVaultModels.CreditCard.Columns.expirationYear.name] ]) } - + // 4. Drop the old database: try database.drop(table: oldTableName) } - + static func migrateV7(database: Database) throws { try database.alter(table: SecureVaultModels.WebsiteAccount.databaseTableName) { $0.add(column: SecureVaultModels.WebsiteAccount.Columns.notes.name, .text) } } - + static func migrateV8(database: Database) throws { try database.alter(table: SecureVaultModels.WebsiteAccount.databaseTableName) { $0.add(column: SecureVaultModels.WebsiteAccount.Columns.signature.name, .text) @@ -913,9 +912,9 @@ extension DefaultAutofillDatabaseProvider { ifNotExists: false ) } - + static func migrateV11(database: Database) throws { - + // Remove WWW from titles and ignore titles containing known export format let accountRows = try Row.fetchCursor(database, sql: "SELECT * FROM \(SecureVaultModels.WebsiteAccount.databaseTableName)") while let accountRow = try accountRows.next() { @@ -925,9 +924,9 @@ extension DefaultAutofillDatabaseProvider { domain: accountRow[SecureVaultModels.WebsiteAccount.Columns.domain.name], created: accountRow[SecureVaultModels.WebsiteAccount.Columns.created.name], lastUpdated: accountRow[SecureVaultModels.WebsiteAccount.Columns.lastUpdated.name]) - + let cleanTitle = account.patternMatchedTitle() - + // Update the accounts table with the new hash value try database.execute(sql: """ UPDATE @@ -937,7 +936,7 @@ extension DefaultAutofillDatabaseProvider { WHERE \(SecureVaultModels.WebsiteAccount.Columns.id.name) = ? """, arguments: [cleanTitle, account.id]) - + } } @@ -1009,10 +1008,10 @@ extension DefaultAutofillDatabaseProvider { // MARK: - Utility functions struct MigrationUtility { - + static func l2encrypt(data: Data, cryptoProvider: SecureStorageCryptoProvider, keyStoreProvider: SecureStorageKeyStoreProvider) throws -> Data { let (crypto, keyStore) = try AutofillSecureVaultFactory.createAndInitializeEncryptionProviders() - + guard let generatedPassword = try keyStore.generatedPassword() else { throw SecureStorageError.noL2Key } @@ -1024,13 +1023,13 @@ struct MigrationUtility { } let decryptedL2Key = try crypto.decrypt(encryptedL2Key, withKey: decryptionKey) - + return try crypto.encrypt(data, withKey: decryptedL2Key) } - + static func l2decrypt(data: Data, cryptoProvider: SecureStorageCryptoProvider, keyStoreProvider: SecureStorageKeyStoreProvider) throws -> Data { let (crypto, keyStore) = (cryptoProvider, keyStoreProvider) - + guard let generatedPassword = try keyStore.generatedPassword() else { throw SecureStorageError.noL2Key } @@ -1071,7 +1070,7 @@ extension SecureVaultModels.WebsiteAccount: PersistableRecord, FetchableRecord { container[Columns.title] = title container[Columns.username] = username container[Columns.domain] = domain - container[Columns.signature] = signature + container[Columns.signature] = signature container[Columns.notes] = notes container[Columns.created] = created container[Columns.lastUpdated] = Date() @@ -1118,7 +1117,7 @@ extension SecureVaultModels.CreditCard: PersistableRecord, FetchableRecord { case title case created case lastUpdated - + case cardNumberData case cardSuffix case cardholderName @@ -1127,7 +1126,7 @@ extension SecureVaultModels.CreditCard: PersistableRecord, FetchableRecord { case expirationMonth case expirationYear } - + enum DeprecatedColumns: String, ColumnExpression { case cardNumber } @@ -1176,7 +1175,7 @@ extension SecureVaultModels.Note: PersistableRecord, FetchableRecord { lastUpdated = row[Columns.lastUpdated] associatedDomain = row[Columns.associatedDomain] text = row[Columns.text] - + displayTitle = generateDisplayTitle() displaySubtitle = generateDisplaySubtitle() } @@ -1246,7 +1245,7 @@ extension SecureVaultModels.Identity: PersistableRecord, FetchableRecord { homePhone = row[Columns.homePhone] mobilePhone = row[Columns.mobilePhone] emailAddress = row[Columns.emailAddress] - + autofillEqualityName = normalizedAutofillName() autofillEqualityAddressStreet = addressStreet?.autofillNormalized() } diff --git a/Sources/BrowserServicesKit/SecureVault/AutofillKeyStoreProvider.swift b/Sources/BrowserServicesKit/SecureVault/AutofillKeyStoreProvider.swift index 609c60fc9..3f211aeae 100644 --- a/Sources/BrowserServicesKit/SecureVault/AutofillKeyStoreProvider.swift +++ b/Sources/BrowserServicesKit/SecureVault/AutofillKeyStoreProvider.swift @@ -56,9 +56,9 @@ final class AutofillKeyStoreProvider: SecureStorageKeyStoreProvider { var query = attributesForEntry(named: name, serviceName: serviceName) query[kSecReturnData as String] = true query[kSecAttrService as String] = serviceName - + var item: CFTypeRef? - + let status = SecItemCopyMatching(query as CFDictionary, &item) switch status { case errSecSuccess: @@ -75,15 +75,15 @@ final class AutofillKeyStoreProvider: SecureStorageKeyStoreProvider { } return data } - + case errSecItemNotFound: - + // Look for an older key and try to migrate if serviceName == Constants.defaultServiceName { return try? migrateV1Key(name: name) } return nil - + default: throw SecureStorageError.keystoreError(status: status) } diff --git a/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift b/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift index a0a3256e4..85e4cdd62 100644 --- a/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift +++ b/Sources/BrowserServicesKit/SecureVault/AutofillSecureVault.swift @@ -21,8 +21,6 @@ import Common import GRDB import SecureStorage -// swiftlint:disable file_length type_body_length - public typealias AutofillVaultFactory = SecureVaultFactory> // swiftlint:disable:next identifier_name @@ -54,7 +52,7 @@ public protocol AutofillSecureVault: SecureVault { func authWith(password: Data) throws -> any AutofillSecureVault func resetL2Password(oldPassword: Data?, newPassword: Data) throws - + func accounts() throws -> [SecureVaultModels.WebsiteAccount] func accountsFor(domain: String) throws -> [SecureVaultModels.WebsiteAccount] func accountsWithPartialMatchesFor(eTLDplus1: String) throws -> [SecureVaultModels.WebsiteAccount] @@ -458,10 +456,10 @@ public class DefaultAutofillSecureVault: AutofillSe try self.providers.database.deleteIdentityForIdentityId(identityId) } } - + public func existingIdentityForAutofill(matching proposedIdentity: SecureVaultModels.Identity) throws -> SecureVaultModels.Identity? { let identities = try self.identities() - + return identities.first { existingIdentity in existingIdentity.hasAutofillEquality(comparedTo: proposedIdentity) } @@ -472,14 +470,14 @@ public class DefaultAutofillSecureVault: AutofillSe public func creditCards() throws -> [SecureVaultModels.CreditCard] { return try executeThrowingDatabaseOperation { let cards = try self.providers.database.creditCards() - + let decryptedCards: [SecureVaultModels.CreditCard] = try cards.map { card in var mutableCard = card mutableCard.cardNumberData = try self.l2Decrypt(data: mutableCard.cardNumberData) - + return mutableCard } - + return decryptedCards } } @@ -495,10 +493,10 @@ public class DefaultAutofillSecureVault: AutofillSe return card } } - + public func existingCardForAutofill(matching proposedCard: SecureVaultModels.CreditCard) throws -> SecureVaultModels.CreditCard? { let cards = try self.creditCards() - + return cards.first { existingCard in existingCard.hasAutofillEquality(comparedTo: proposedCard) } @@ -508,10 +506,10 @@ public class DefaultAutofillSecureVault: AutofillSe public func storeCreditCard(_ card: SecureVaultModels.CreditCard) throws -> Int64 { return try executeThrowingDatabaseOperation { var mutableCard = card - + mutableCard.cardSuffix = SecureVaultModels.CreditCard.suffix(from: mutableCard.cardNumber) mutableCard.cardNumberData = try self.l2Encrypt(data: mutableCard.cardNumberData) - + return try self.providers.database.storeCreditCard(mutableCard) } } @@ -596,7 +594,7 @@ public class DefaultAutofillSecureVault: AutofillSe } return try providers.crypto.decrypt(encryptedL2Key, withKey: decryptionKey) } - + private func l2Encrypt(data: Data, using l2Key: Data? = nil) throws -> Data { let key: Data = try { if let l2Key { @@ -619,5 +617,3 @@ public class DefaultAutofillSecureVault: AutofillSe return try providers.crypto.decrypt(data, withKey: key) } } - -// swiftlint:enable file_length type_body_length diff --git a/Sources/BrowserServicesKit/SecureVault/CredentialsDatabaseCleaner.swift b/Sources/BrowserServicesKit/SecureVault/CredentialsDatabaseCleaner.swift index ce6f4b232..04bb3c61a 100644 --- a/Sources/BrowserServicesKit/SecureVault/CredentialsDatabaseCleaner.swift +++ b/Sources/BrowserServicesKit/SecureVault/CredentialsDatabaseCleaner.swift @@ -1,6 +1,5 @@ // // CredentialsDatabaseCleaner.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift b/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift index 3b7b44652..67cb3f0bb 100644 --- a/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift +++ b/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift @@ -1,6 +1,5 @@ // // SecureVaultManager.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -22,8 +21,6 @@ import Combine import Common import SecureStorage -// swiftlint:disable file_length - public enum AutofillType { case password case card @@ -38,9 +35,9 @@ public struct AutofillData { } public protocol SecureVaultManagerDelegate: AnyObject, SecureVaultErrorReporting { - + func secureVaultManagerIsEnabledStatus(_ manager: SecureVaultManager, forType type: AutofillType?) -> Bool - + func secureVaultManagerShouldSaveData(_: SecureVaultManager) -> Bool func secureVaultManager(_: SecureVaultManager, @@ -56,11 +53,11 @@ public protocol SecureVaultManagerDelegate: AnyObject, SecureVaultErrorReporting func secureVaultManager(_: SecureVaultManager, promptUserWithGeneratedPassword password: String, completionHandler: @escaping (Bool) -> Void) - + func secureVaultManager(_: SecureVaultManager, isAuthenticatedFor type: AutofillType, completionHandler: @escaping (Bool) -> Void) - + func secureVaultManager(_: SecureVaultManager, didAutofill type: AutofillType, withObjectId objectId: String) func secureVaultManager(_: SecureVaultManager, didRequestAuthenticationWithCompletionHandler: @escaping (Bool) -> Void) @@ -104,7 +101,7 @@ public protocol PasswordManager: AnyObject { public class SecureVaultManager { public weak var delegate: SecureVaultManagerDelegate? - + private let vault: (any AutofillSecureVault)? // Third party password manager @@ -117,7 +114,7 @@ public class SecureVaultManager { // Keeps track of partial account created from autogenerated credentials (Private Email + Pwd) private var autosaveAccount: SecureVaultModels.WebsiteAccount? - + // Tracks if the autosave account was created in the current session // To prevent automatically updating username/password for an an existing account private var autosaveAccountCreatedInSession = false @@ -127,7 +124,7 @@ public class SecureVaultManager { private var autogeneratedPassword: Bool = false private var autogeneratedCredentials: Bool { return autogeneratedUserName || autogeneratedPassword - } + } public lazy var autofillWebsiteAccountMatcher: AutofillWebsiteAccountMatcher? = { guard let tld = tld else { return nil } @@ -163,17 +160,17 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { return } let vault = try self.vault ?? AutofillSecureVaultFactory.makeVault(errorReporter: self.delegate) - + var identities: [SecureVaultModels.Identity] = [] if delegate.secureVaultManagerIsEnabledStatus(self, forType: .identity) { identities = try vault.identities() } - + var cards: [SecureVaultModels.CreditCard] = [] if delegate.secureVaultManagerIsEnabledStatus(self, forType: .card) { cards = try vault.creditCards() } - + if delegate.secureVaultManagerIsEnabledStatus(self, forType: .password) { getAccounts(for: domain, from: vault, @@ -200,7 +197,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { public func autofillUserScript(_: AutofillUserScript, didRequestCreditCardsManagerForDomain domain: String) { delegate?.secureVaultManager(self, didRequestCreditCardsManagerForDomain: domain) } - + public func autofillUserScript(_: AutofillUserScript, didRequestIdentitiesManagerForDomain domain: String) { delegate?.secureVaultManager(self, didRequestIdentitiesManagerForDomain: domain) } @@ -222,13 +219,13 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { var autofilldata = data let vault = try? self.vault ?? AutofillSecureVaultFactory.makeVault(errorReporter: self.delegate) - var autoSavedCredentials: SecureVaultModels.WebsiteCredentials? - + var autoSavedCredentials: SecureVaultModels.WebsiteCredentials? + // If the user navigated away from current domain, remove autosave data if domain != autosaveAccount?.domain { autogeneratedUserName = false - autogeneratedPassword = false - autosaveAccount = nil + autogeneratedPassword = false + autosaveAccount = nil autosaveAccountCreatedInSession = false } @@ -262,11 +259,11 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { let existingPassword = credentials.password.flatMap { String(decoding: $0, as: UTF8.self) } let submittedUserName = data.credentials?.username let submittedPassword = data.credentials?.password - + if existingUsername != submittedUserName && submittedUserName != "" { autogeneratedUserName = false } - + if existingPassword != submittedPassword && submittedPassword != "" { autogeneratedPassword = false } @@ -342,7 +339,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { } } - + public func autofillUserScript(_: AutofillUserScript, didRequestCredentialsForDomain domain: String, subType: AutofillUserScript.GetAutofillDataSubType, @@ -410,7 +407,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { do { let vault = try self.vault ?? AutofillSecureVaultFactory.makeVault(errorReporter: self.delegate) - + self.delegate?.secureVaultManager(self, isAuthenticatedFor: .password, completionHandler: { result in if result == true { self.getCredentials(for: accountId, from: vault, or: self.passwordManager) { [weak self] credentials, error in @@ -426,8 +423,8 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { } else { return } - }) - + }) + } catch { os_log(.error, "Error requesting credentials: %{public}@", error.localizedDescription) completionHandler(nil, credentialsProvider) @@ -449,7 +446,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { completionHandler(nil) } }) - + delegate?.secureVaultManager(self, didAutofill: .card, withObjectId: String(creditCardId)) } catch { os_log(.error, "Error requesting credit card: %{public}@", error.localizedDescription) @@ -530,7 +527,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { completionHandler(useGeneratedPassword) } } - + public func autofillUserScript(_: AutofillUserScript, didSendPixel pixel: AutofillUserScript.JSPixel) { delegate?.secureVaultManager(self, didReceivePixel: pixel) } @@ -551,7 +548,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { os_log("Did not meet conditions for silently saving autogenerated credentials, returning early", log: .passwordManager) return } - + let user: String = credentials.username ?? "" let pass: String = credentials.password ?? "" @@ -579,7 +576,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { // created in this visit to the page, and the user has not navigated anywhere. if autosaveAccountCreatedInSession { if let id = currentAccount.id { - + // Update password if provided, and existing account does not have one let pwd = credentials.password ?? "" var pwdData: Data @@ -589,15 +586,15 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { } else { pwdData = Data(pwd.utf8) } - + // Update username if provided if user != "" { currentAccount.username = user } - - // Save + + // Save try vault.storeWebsiteCredentials(SecureVaultModels.WebsiteCredentials(account: currentAccount, password: pwdData)) - + // Update the autosave account with changes autosaveAccount = currentAccount } @@ -619,7 +616,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { autofillData: AutofillUserScript.DetectedAutofillData ) throws -> AutofillData { let vault = try self.vault ?? AutofillSecureVaultFactory.makeVault(errorReporter: self.delegate) - + let proposedIdentity = try existingIdentity(with: autofillData, vault: vault) let proposedCredentials: SecureVaultModels.WebsiteCredentials? if let passwordManager = passwordManager, passwordManager.isEnabled { @@ -633,13 +630,13 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { } let proposedCard = try existingPaymentMethod(with: autofillData, vault: vault) - + return AutofillData(identity: proposedIdentity, credentials: proposedCredentials, creditCard: proposedCard, automaticallySavedCredentials: autofillData.hasAutogeneratedCredentials) } - + private func existingIdentity(with autofillData: AutofillUserScript.DetectedAutofillData, vault: any AutofillSecureVault) throws -> SecureVaultModels.Identity? { if let identity = autofillData.identity, try vault.existingIdentityForAutofill(matching: identity) == nil { @@ -650,7 +647,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { return nil } } - + private func existingCredentials(with autofillData: AutofillUserScript.DetectedAutofillData, domain: String, vault: any AutofillSecureVault) throws -> SecureVaultModels.WebsiteCredentials? { @@ -659,11 +656,11 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { let passwordData = credentials.password?.data(using: .utf8) else { return nil } - + guard let accounts = try? vault.accountsFor(domain: domain), // Matching account (username) or account with empty username for domain var account = accounts.first(where: { $0.username == credentials.username || $0.username == "" }) else { - + // No existing credentials found. This is a new entry let account = SecureVaultModels.WebsiteAccount(username: credentials.username ?? "", domain: domain) return SecureVaultModels.WebsiteCredentials(account: account, password: passwordData) @@ -696,7 +693,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { return nil } - + private func existingPaymentMethod(with autofillData: AutofillUserScript.DetectedAutofillData, vault: any AutofillSecureVault) throws -> SecureVaultModels.CreditCard? { if let card = autofillData.creditCard, try vault.existingCardForAutofill(matching: card) == nil { @@ -810,5 +807,3 @@ fileprivate extension AutofillSecureVault { } } - -// swiftlint:enable file_length diff --git a/Sources/BrowserServicesKit/SecureVault/SecureVaultModels+Sync.swift b/Sources/BrowserServicesKit/SecureVault/SecureVaultModels+Sync.swift index 16c8299c8..d9caea156 100644 --- a/Sources/BrowserServicesKit/SecureVault/SecureVaultModels+Sync.swift +++ b/Sources/BrowserServicesKit/SecureVault/SecureVaultModels+Sync.swift @@ -1,6 +1,5 @@ // // SecureVaultModels+Sync.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/SecureVault/SecureVaultModels.swift b/Sources/BrowserServicesKit/SecureVault/SecureVaultModels.swift index d9a35ccce..94ef9e4f9 100644 --- a/Sources/BrowserServicesKit/SecureVault/SecureVaultModels.swift +++ b/Sources/BrowserServicesKit/SecureVault/SecureVaultModels.swift @@ -19,15 +19,13 @@ import Foundation import Common -// swiftlint:disable file_length type_body_length - /// The models used by the secure vault. -/// +/// /// Future models include: /// * Generated Password - a password generated for a site, but not used yet /// * Duck Address - a duck address used on a partcular site public struct SecureVaultModels { - + /// A username and password was saved for a given site. Password is stored seperately so that /// it can be queried independently. public struct WebsiteCredentials { @@ -52,7 +50,7 @@ public struct SecureVaultModels { public var notes: String? public let created: Date public let lastUpdated: Date - + public enum CommonTitlePatterns: String, CaseIterable { /* Matches the following title patterns @@ -120,7 +118,7 @@ public struct SecureVaultModels { } return hash } - + public func name(tld: TLD, autofillDomainNameUrlMatcher: AutofillDomainNameUrlMatcher) -> String { if let title = self.title, !title.isEmpty { return title @@ -128,11 +126,11 @@ public struct SecureVaultModels { return autofillDomainNameUrlMatcher.normalizeUrlForWeb(domain ?? "") } } - + public func firstTLDLetter(tld: TLD, autofillDomainNameUrlSort: AutofillDomainNameUrlSort) -> String? { return autofillDomainNameUrlSort.firstCharacterForGrouping(self, tld: tld)?.uppercased() } - + public func patternMatchedTitle() -> String { guard let title = title, !title.isEmpty else { return "" @@ -155,7 +153,7 @@ public struct SecureVaultModels { } } } - + // If no pattern matched, return the original title return title } @@ -632,5 +630,3 @@ private extension Date { return calendar.date(from: dateComponents) ?? self } } - -// swiftlint:enable file_length type_body_length diff --git a/Sources/BrowserServicesKit/SmarterEncryption/EmbeddedBloomFilterResources.swift b/Sources/BrowserServicesKit/SmarterEncryption/EmbeddedBloomFilterResources.swift index 17b41914e..c0bb6b898 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/EmbeddedBloomFilterResources.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/EmbeddedBloomFilterResources.swift @@ -1,6 +1,5 @@ // // EmbeddedBloomFilterResources.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSBloomFilterSpecification.swift b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSBloomFilterSpecification.swift index 6ee65c702..5b0a0c0f2 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSBloomFilterSpecification.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSBloomFilterSpecification.swift @@ -19,12 +19,12 @@ import Foundation public struct HTTPSBloomFilterSpecification: Equatable, Decodable, Sendable { - + public let bitCount: Int public let errorRate: Double public let totalEntries: Int public let sha256: String - + public init(bitCount: Int, errorRate: Double, totalEntries: Int, sha256: String) { self.bitCount = bitCount self.errorRate = errorRate diff --git a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSExcludedDomains.swift b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSExcludedDomains.swift index 5005b081d..6eb1d5331 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSExcludedDomains.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSExcludedDomains.swift @@ -1,6 +1,5 @@ // // HTTPSExcludedDomains.swift -// Core // // Copyright © 2020 DuckDuckGo. All rights reserved. // @@ -20,7 +19,7 @@ import Foundation public struct HTTPSExcludedDomains: Decodable { - + public let data: [String] - + } diff --git a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgrade.swift b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgrade.swift index 7d461d2fb..6d7fad50d 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgrade.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgrade.swift @@ -1,6 +1,5 @@ // -// ContentBlockerDebugEvents.swift -// DuckDuckGo +// HTTPSUpgrade.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeParser.swift b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeParser.swift index 319f5a5a9..374ae5e92 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeParser.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeParser.swift @@ -1,6 +1,5 @@ // // HTTPSUpgradeParser.swift -// DuckDuckGo // // Copyright © 2018 DuckDuckGo. All rights reserved. // @@ -21,7 +20,7 @@ import Foundation import Common public final class HTTPSUpgradeParser { - + public static func convertExcludedDomainsData(_ data: Data) throws -> [String] { do { let decoder = JSONDecoder() @@ -32,7 +31,7 @@ public final class HTTPSUpgradeParser { throw JsonError.typeMismatch } } - + public static func convertBloomFilterSpecification(fromJSONData data: Data) throws -> HTTPSBloomFilterSpecification { do { let decoder = JSONDecoder() diff --git a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeStore.swift b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeStore.swift index 99bdb8a07..241e83cb4 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeStore.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/HTTPSUpgradeStore.swift @@ -25,7 +25,7 @@ public protocol HTTPSUpgradeStore { func loadBloomFilter() -> BloomFilter? func persistBloomFilter(specification: HTTPSBloomFilterSpecification, data: Data) throws - + // MARK: - Excluded domains func hasExcludedDomain(_ domain: String) -> Bool diff --git a/Sources/BrowserServicesKit/SmarterEncryption/Store/AppHTTPSUpgradeStore.swift b/Sources/BrowserServicesKit/SmarterEncryption/Store/AppHTTPSUpgradeStore.swift index 714f5b1f5..d2f96a8d2 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/Store/AppHTTPSUpgradeStore.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/Store/AppHTTPSUpgradeStore.swift @@ -1,6 +1,5 @@ // // AppHTTPSUpgradeStore.swift -// DuckDuckGo // // Copyright © 2018 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSExcludedDomain.swift b/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSExcludedDomain.swift index c1b927c02..3353061ae 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSExcludedDomain.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSExcludedDomain.swift @@ -1,6 +1,5 @@ // // HTTPSExcludedDomain.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,7 +21,7 @@ import CoreData @objc(HTTPSExcludedDomain) public class HTTPSExcludedDomain: NSManagedObject { - + @nonobjc public class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: "HTTPSExcludedDomain") } diff --git a/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSStoredBloomFilterSpecification.swift b/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSStoredBloomFilterSpecification.swift index d39b1a769..d92da267c 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSStoredBloomFilterSpecification.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSStoredBloomFilterSpecification.swift @@ -1,6 +1,5 @@ // // HTTPSStoredBloomFilterSpecification.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSUpgradeManagedObjectModel.swift b/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSUpgradeManagedObjectModel.swift index fe30e0a4d..7c905d284 100644 --- a/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSUpgradeManagedObjectModel.swift +++ b/Sources/BrowserServicesKit/SmarterEncryption/Store/HTTPSUpgradeManagedObjectModel.swift @@ -1,6 +1,5 @@ // // HTTPSUpgradeManagedObjectModel.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/Statistics/StatisticsStore.swift b/Sources/BrowserServicesKit/Statistics/StatisticsStore.swift index 931078c6e..036ec5a37 100644 --- a/Sources/BrowserServicesKit/Statistics/StatisticsStore.swift +++ b/Sources/BrowserServicesKit/Statistics/StatisticsStore.swift @@ -1,6 +1,5 @@ // // StatisticsStore.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/BrowserServicesKit/Statistics/VariantManager.swift b/Sources/BrowserServicesKit/Statistics/VariantManager.swift index 13deb37c6..7a4b07561 100644 --- a/Sources/BrowserServicesKit/Statistics/VariantManager.swift +++ b/Sources/BrowserServicesKit/Statistics/VariantManager.swift @@ -1,6 +1,5 @@ // // VariantManager.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -21,16 +20,16 @@ import Foundation /// Define new experimental features by extending the struct in client project. public struct FeatureName: RawRepresentable, Equatable { - + public var rawValue: String // Used for unit tests public static let dummy = FeatureName(rawValue: "dummy") - + public init(rawValue: String) { self.rawValue = rawValue } - + } public protocol VariantManager { diff --git a/Sources/BrowserServicesKit/Suggestions/Suggestion.swift b/Sources/BrowserServicesKit/Suggestions/Suggestion.swift index 90261c38a..2303dfda5 100644 --- a/Sources/BrowserServicesKit/Suggestions/Suggestion.swift +++ b/Sources/BrowserServicesKit/Suggestions/Suggestion.swift @@ -19,7 +19,7 @@ import Foundation public enum Suggestion: Equatable { - + case phrase(phrase: String) case website(url: URL) case bookmark(title: String, url: URL, isFavorite: Bool, allowedInTopHits: Bool) diff --git a/Sources/BrowserServicesKit/Suggestions/SuggestionProcessing.swift b/Sources/BrowserServicesKit/Suggestions/SuggestionProcessing.swift index 91f89c021..398a0a78f 100644 --- a/Sources/BrowserServicesKit/Suggestions/SuggestionProcessing.swift +++ b/Sources/BrowserServicesKit/Suggestions/SuggestionProcessing.swift @@ -103,7 +103,7 @@ final class SuggestionProcessing { default: score = 0 } - + return (item, score) } // Filter not relevant @@ -111,7 +111,7 @@ final class SuggestionProcessing { // Sort according to the score .sorted { $0.score > $1.score } // Create suggestion array - .compactMap { + .compactMap { switch $0.item { case let bookmark as Bookmark: return Suggestion(bookmark: bookmark) diff --git a/Sources/BrowserServicesKit/Suggestions/SuggestionResult.swift b/Sources/BrowserServicesKit/Suggestions/SuggestionResult.swift index f0f4fe1d3..a7aa1d872 100644 --- a/Sources/BrowserServicesKit/Suggestions/SuggestionResult.swift +++ b/Sources/BrowserServicesKit/Suggestions/SuggestionResult.swift @@ -1,5 +1,5 @@ // -// File.swift +// SuggestionResult.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/AppVersion.swift b/Sources/Common/AppVersion.swift index 1e8441a62..385ff9480 100644 --- a/Sources/Common/AppVersion.swift +++ b/Sources/Common/AppVersion.swift @@ -1,6 +1,5 @@ // // AppVersion.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // @@ -22,7 +21,7 @@ import Foundation public struct AppVersion { public static let shared = AppVersion() - + private let bundle: InfoBundle public init(bundle: InfoBundle = Bundle.main) { @@ -36,7 +35,7 @@ public struct AppVersion { public var identifier: String { return bundle.object(forInfoDictionaryKey: Bundle.Key.identifier) as? String ?? "" } - + public var majorVersionNumber: String { return String(versionNumber.split(separator: ".").first ?? "") } @@ -48,11 +47,11 @@ public struct AppVersion { public var buildNumber: String { return bundle.object(forInfoDictionaryKey: Bundle.Key.buildNumber) as? String ?? "" } - + public var versionAndBuildNumber: String { return "\(versionNumber).\(buildNumber)" } - + public var localized: String { return "\(name) \(versionAndBuildNumber)" } @@ -61,5 +60,5 @@ public struct AppVersion { let os = ProcessInfo().operatingSystemVersion return "\(os.majorVersion).\(os.minorVersion).\(os.patchVersion)" } - + } diff --git a/Sources/Common/Combine/ScheduledFuture.swift b/Sources/Common/Combine/ScheduledFuture.swift index 265f6a02a..ecd345417 100644 --- a/Sources/Common/Combine/ScheduledFuture.swift +++ b/Sources/Common/Combine/ScheduledFuture.swift @@ -1,5 +1,5 @@ // -// ScheduledFuture.swift.swift +// ScheduledFuture.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/Debug.swift b/Sources/Common/Debug.swift index 9759a4698..8b8a33946 100644 --- a/Sources/Common/Debug.swift +++ b/Sources/Common/Debug.swift @@ -1,6 +1,5 @@ // // Debug.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -18,7 +17,6 @@ // import Foundation -import os.log #if DEBUG diff --git a/Sources/Common/DecodableHelper.swift b/Sources/Common/DecodableHelper.swift index e72a85bed..5a66476be 100644 --- a/Sources/Common/DecodableHelper.swift +++ b/Sources/Common/DecodableHelper.swift @@ -17,7 +17,6 @@ // import Foundation -import os.log public struct DecodableHelper { public static func decode(from input: Input) -> Target? { diff --git a/Sources/Common/EventMapping.swift b/Sources/Common/EventMapping.swift index 9141fbd31..b152f7370 100644 --- a/Sources/Common/EventMapping.swift +++ b/Sources/Common/EventMapping.swift @@ -1,6 +1,5 @@ // // EventMapping.swift -// DuckDuckGo // // Copyright © 2019 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/Extensions/ArrayExtension.swift b/Sources/Common/Extensions/ArrayExtension.swift index 321d39f4c..785715d72 100644 --- a/Sources/Common/Extensions/ArrayExtension.swift +++ b/Sources/Common/Extensions/ArrayExtension.swift @@ -1,6 +1,5 @@ // // ArrayExtension.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/Extensions/BundleExtension.swift b/Sources/Common/Extensions/BundleExtension.swift index e8fb9e54b..79500e3dc 100644 --- a/Sources/Common/Extensions/BundleExtension.swift +++ b/Sources/Common/Extensions/BundleExtension.swift @@ -1,6 +1,5 @@ // // BundleExtension.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -22,7 +21,7 @@ import Foundation extension Bundle { enum Key { - + static let name = kCFBundleNameKey as String static let identifier = kCFBundleIdentifierKey as String static let buildNumber = kCFBundleVersionKey as String diff --git a/Sources/Common/Extensions/CalendarExtension.swift b/Sources/Common/Extensions/CalendarExtension.swift index 3d8e5d33d..cf18a7728 100644 --- a/Sources/Common/Extensions/CalendarExtension.swift +++ b/Sources/Common/Extensions/CalendarExtension.swift @@ -1,6 +1,5 @@ // // CalendarExtension.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -24,5 +23,5 @@ extension Calendar { let numberOfDays = dateComponents([.day], from: from, to: to) return numberOfDays.day } - + } diff --git a/Sources/Common/Extensions/FileManagerExtension.swift b/Sources/Common/Extensions/FileManagerExtension.swift index b2a3a40c6..0ce9d419b 100644 --- a/Sources/Common/Extensions/FileManagerExtension.swift +++ b/Sources/Common/Extensions/FileManagerExtension.swift @@ -1,6 +1,5 @@ // // FileManagerExtension.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -20,7 +19,7 @@ import Foundation extension FileManager { - + public func applicationSupportDirectoryForComponent(named name: String) -> URL { #if os(macOS) let sandboxPathComponent = "Containers/\(Bundle.main.bundleIdentifier!)/Data/Library/Application Support/" @@ -33,5 +32,5 @@ extension FileManager { #endif return dir.appendingPathComponent(name) } - + } diff --git a/Sources/Common/Extensions/HashExtension.swift b/Sources/Common/Extensions/HashExtension.swift index 45639ce34..b6752cf57 100644 --- a/Sources/Common/Extensions/HashExtension.swift +++ b/Sources/Common/Extensions/HashExtension.swift @@ -1,6 +1,5 @@ // // HashExtension.swift -// DuckDuckGo // // Copyright © 2019 DuckDuckGo. All rights reserved. // @@ -21,17 +20,17 @@ import Foundation import CommonCrypto extension Data { - + private typealias Algorithm = (UnsafeRawPointer?, CC_LONG, UnsafeMutablePointer?) -> UnsafeMutablePointer? - + public var sha1: String { return hash(algorithm: CC_SHA1, length: CC_SHA1_DIGEST_LENGTH) } - + public var sha256: String { return hash(algorithm: CC_SHA256, length: CC_SHA256_DIGEST_LENGTH) } - + private func hash(algorithm: Algorithm, length: Int32) -> String { var hash = [UInt8](repeating: 0, count: Int(length)) let dataBytes = [UInt8](self) @@ -41,10 +40,10 @@ extension Data { } extension String { - + public var sha1: String { let dataBytes = data(using: .utf8)! return dataBytes.sha1 } - + } diff --git a/Sources/Common/Extensions/JSONExtensions.swift b/Sources/Common/Extensions/JSONExtensions.swift index 4746cf74a..d75524bbc 100644 --- a/Sources/Common/Extensions/JSONExtensions.swift +++ b/Sources/Common/Extensions/JSONExtensions.swift @@ -1,6 +1,5 @@ // // JSONExtensions.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/Extensions/NSObjectExtension.swift b/Sources/Common/Extensions/NSObjectExtension.swift index 816f8098b..783bfecfa 100644 --- a/Sources/Common/Extensions/NSObjectExtension.swift +++ b/Sources/Common/Extensions/NSObjectExtension.swift @@ -1,6 +1,5 @@ // // NSObjectExtension.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/Extensions/RunLoopExtension.swift b/Sources/Common/Extensions/RunLoopExtension.swift index abb13516b..54058b3e8 100644 --- a/Sources/Common/Extensions/RunLoopExtension.swift +++ b/Sources/Common/Extensions/RunLoopExtension.swift @@ -1,6 +1,5 @@ // // RunLoopExtension.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/Extensions/StringExtension.swift b/Sources/Common/Extensions/StringExtension.swift index 112c83cd7..613c4cb4c 100644 --- a/Sources/Common/Extensions/StringExtension.swift +++ b/Sources/Common/Extensions/StringExtension.swift @@ -74,16 +74,16 @@ public extension String { } return self } - + func autofillNormalized() -> String { let autofillCharacterSet = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters).union(.symbols) - + var normalizedString = self normalizedString = normalizedString.removingCharacters(in: autofillCharacterSet) normalizedString = normalizedString.folding(options: .diacriticInsensitive, locale: .current) normalizedString = normalizedString.localizedLowercase - + return normalizedString } diff --git a/Sources/Common/Extensions/URLExtension.swift b/Sources/Common/Extensions/URLExtension.swift index 8739d1644..786bf87d4 100644 --- a/Sources/Common/Extensions/URLExtension.swift +++ b/Sources/Common/Extensions/URLExtension.swift @@ -18,7 +18,6 @@ import Foundation -// swiftlint:disable file_length extension URL { public static let empty = (NSURL(string: "") ?? NSURL()) as URL @@ -59,7 +58,7 @@ extension URL { components.password = nil return components.url } - + public var isRoot: Bool { (path.isEmpty || path == "/") && query == nil && @@ -73,7 +72,7 @@ extension URL { host: self.host ?? "", port: self.port ?? 0) } - + public func isPart(ofDomain domain: String) -> Bool { guard let host = host else { return false } return host == domain || host.hasSuffix(".\(domain)") @@ -112,7 +111,7 @@ extension URL { public static var schemesWithRemovableBasicAuth: [NavigationalScheme] { return [.http, .https, .ftp, .file] } - + public static var hypertextSchemes: [NavigationalScheme] { return [.http, .https] } @@ -240,7 +239,6 @@ extension URL { self.init(string: url) } - // swiftlint:disable:next large_tuple private static func fixupAndSplitURLString(_ s: String) -> (authData: String.SubSequence?, domainAndPath: String.SubSequence, query: String)? { let urlAndFragment = s.split(separator: "#", maxSplits: 1) guard !urlAndFragment.isEmpty else { return nil } @@ -284,15 +282,15 @@ extension URL { domainAndPath: urlAndQuery[0], query: query) } - + public func replacing(host: String?) -> URL? { guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return self } components.host = host return components.url } - + // MARK: - HTTP/HTTPS - + public enum URLProtocol: String { case http case https @@ -308,20 +306,20 @@ extension URL { components.scheme = URLProtocol.https.rawValue return components.url } - + public var isHttp: Bool { scheme == "http" } - + public var isHttps: Bool { scheme == "https" } // MARK: - Parameters - + public func appendingParameters(_ parameters: QueryParams, allowedReservedCharacters: CharacterSet? = nil) -> URL where QueryParams.Element == (key: String, value: String) { - + return parameters.reduce(self) { partialResult, parameter in partialResult.appendingParameter( name: parameter.key, @@ -358,7 +356,7 @@ extension URL { }) return queryItem?.value } - + public func isThirdParty(to otherUrl: URL, tld: TLD) -> Bool { guard let thisHost = host else { return false @@ -368,7 +366,7 @@ extension URL { } let thisRoot = tld.eTLDplus1(thisHost) let otherRoot = tld.eTLDplus1(otherHost) - + return thisRoot != otherRoot } @@ -452,4 +450,3 @@ extension URLQueryItem { } } -// swiftlint:enable file_length diff --git a/Sources/Common/Extensions/UnsafeMutableRawPointerExtension.swift b/Sources/Common/Extensions/UnsafeMutableRawPointerExtension.swift index d1b36bd69..990d58792 100644 --- a/Sources/Common/Extensions/UnsafeMutableRawPointerExtension.swift +++ b/Sources/Common/Extensions/UnsafeMutableRawPointerExtension.swift @@ -1,6 +1,5 @@ // // UnsafeMutableRawPointerExtension.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/InfoBundle.swift b/Sources/Common/InfoBundle.swift index 262fe1cdb..64f957801 100644 --- a/Sources/Common/InfoBundle.swift +++ b/Sources/Common/InfoBundle.swift @@ -1,6 +1,5 @@ // // InfoBundle.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -20,9 +19,9 @@ import Foundation public protocol InfoBundle { - + func object(forInfoDictionaryKey key: String) -> Any? - + } extension Bundle: InfoBundle { } diff --git a/Sources/Common/JsonError.swift b/Sources/Common/JsonError.swift index eb2ad4040..798a3a0de 100644 --- a/Sources/Common/JsonError.swift +++ b/Sources/Common/JsonError.swift @@ -1,6 +1,5 @@ // // JsonError.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // diff --git a/Sources/Common/Logging.swift b/Sources/Common/Logging.swift index 79250940c..fe93cb221 100644 --- a/Sources/Common/Logging.swift +++ b/Sources/Common/Logging.swift @@ -1,6 +1,5 @@ // // Logging.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -18,7 +17,7 @@ // import Foundation -import os +import os // swiftlint:disable:this enforce_os_log_wrapper public typealias OSLog = os.OSLog diff --git a/Sources/Common/TLD/TLD.swift b/Sources/Common/TLD/TLD.swift index 2a7beb2c4..bb3616b9d 100644 --- a/Sources/Common/TLD/TLD.swift +++ b/Sources/Common/TLD/TLD.swift @@ -1,6 +1,5 @@ // // TLD.swift -// DuckDuckGo // // Copyright © 2018 DuckDuckGo. All rights reserved. // @@ -32,13 +31,13 @@ public class TLD { public init() { guard let url = Bundle.module.url(forResource: "tlds", withExtension: "json") else { return } guard let data = try? Data(contentsOf: url) else { return } - + let asString = String(decoding: data, as: UTF8.self) let asStringWithoutComments = asString.replacingOccurrences(of: "(?m)^//.*", with: "", options: .regularExpression) guard let cleanedData: Data = asStringWithoutComments.data(using: .utf8) else { return } - + guard let tlds = try? JSONDecoder().decode([String].self, from: cleanedData) else { return } self.tlds = Set(tlds) } @@ -52,20 +51,20 @@ public class TLD { guard let host = host else { return nil } let parts = [String](host.components(separatedBy: ".").reversed()) - + var stack = "" var knownTLDFound = false for part in parts { stack = !stack.isEmpty ? part + "." + stack : part - + if tlds.contains(stack) { knownTLDFound = true } else if knownTLDFound { break } } - + // If host does not contain tld treat it as invalid if knownTLDFound { return stack @@ -83,7 +82,7 @@ public class TLD { guard let domain = domain(host), !tlds.contains(domain) else { return nil } return domain } - + public func eTLDplus1(forStringURL stringURL: String) -> String? { guard let escapedStringURL = stringURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return nil } guard let host = URL(string: escapedStringURL)?.host else { return nil } diff --git a/Sources/Configuration/Configuration.swift b/Sources/Configuration/Configuration.swift index 9ed575983..b37f55f80 100644 --- a/Sources/Configuration/Configuration.swift +++ b/Sources/Configuration/Configuration.swift @@ -1,6 +1,5 @@ // // Configuration.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,13 +19,13 @@ import Foundation public protocol ConfigurationURLProviding { - + func url(for configuration: Configuration) -> URL - + } public enum Configuration: String, CaseIterable, Sendable { - + case bloomFilterBinary case bloomFilterSpec case bloomFilterExcludedDomains @@ -34,15 +33,15 @@ public enum Configuration: String, CaseIterable, Sendable { case surrogates case trackerDataSet case FBConfig - + private static var urlProvider: ConfigurationURLProviding? public static func setURLProvider(_ urlProvider: ConfigurationURLProviding) { self.urlProvider = urlProvider } - + var url: URL { guard let urlProvider = Self.urlProvider else { fatalError("Please set the urlProvider before accessing url.") } return urlProvider.url(for: self) } - + } diff --git a/Sources/Configuration/ConfigurationFetching.swift b/Sources/Configuration/ConfigurationFetching.swift index e75dd2dc0..bf214ccc0 100644 --- a/Sources/Configuration/ConfigurationFetching.swift +++ b/Sources/Configuration/ConfigurationFetching.swift @@ -1,6 +1,5 @@ // // ConfigurationFetching.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -31,9 +30,9 @@ protocol ConfigurationFetching { typealias ConfigurationFetchResult = (etag: String, data: Data?) public final class ConfigurationFetcher: ConfigurationFetching { - + public enum Error: Swift.Error { - + case apiRequest(APIRequest.Error) case invalidPayload @@ -82,7 +81,7 @@ public final class ConfigurationFetcher: ConfigurationFetching { } try store(fetchResult, for: configuration) } - + /** Downloads and stores the configurations provided in parallel using a throwing task group. This function throws an error if any of the configurations fail to fetch or validate. @@ -120,14 +119,14 @@ public final class ConfigurationFetcher: ConfigurationFetching { } } } - + private func etag(for configuration: Configuration) -> String? { if let etag = store.loadEtag(for: configuration), store.loadData(for: configuration) != nil { return etag } return store.loadEmbeddedEtag(for: configuration) } - + private func fetch(from url: URL, withEtag etag: String?, requirements: APIResponseRequirements) async throws -> ConfigurationFetchResult { let configuration = APIRequest.Configuration(url: url, headers: APIRequest.Headers(etag: etag), @@ -144,5 +143,5 @@ public final class ConfigurationFetcher: ConfigurationFetching { try store.saveEtag(result.etag, for: configuration) } } - + } diff --git a/Sources/Configuration/ConfigurationStoring.swift b/Sources/Configuration/ConfigurationStoring.swift index e6e96cb1c..b44a26ace 100644 --- a/Sources/Configuration/ConfigurationStoring.swift +++ b/Sources/Configuration/ConfigurationStoring.swift @@ -1,6 +1,5 @@ // // ConfigurationStoring.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,12 +19,12 @@ import Foundation public protocol ConfigurationStoring { - + func loadData(for configuration: Configuration) -> Data? func loadEtag(for configuration: Configuration) -> String? func loadEmbeddedEtag(for configuration: Configuration) -> String? - + mutating func saveData(_ data: Data, for configuration: Configuration) throws mutating func saveEtag(_ etag: String, for configuration: Configuration) throws - + } diff --git a/Sources/Configuration/ConfigurationValidating.swift b/Sources/Configuration/ConfigurationValidating.swift index 1be6f7f5d..d1e015969 100644 --- a/Sources/Configuration/ConfigurationValidating.swift +++ b/Sources/Configuration/ConfigurationValidating.swift @@ -22,9 +22,9 @@ import TrackerRadarKit import Common protocol ConfigurationValidating { - + func validate(_ data: Data, for configuration: Configuration) throws - + } public struct ConfigurationValidator: ConfigurationValidating { @@ -58,5 +58,5 @@ public struct ConfigurationValidator: ConfigurationValidating { private func validateTrackerDataSet(with data: Data) throws { _ = try JSONDecoder().decode(TrackerData.self, from: data) } - + } diff --git a/Sources/ContentBlocking/DetectedRequest.swift b/Sources/ContentBlocking/DetectedRequest.swift index b87c389dd..935370a14 100644 --- a/Sources/ContentBlocking/DetectedRequest.swift +++ b/Sources/ContentBlocking/DetectedRequest.swift @@ -1,6 +1,5 @@ // // DetectedRequest.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // @@ -32,10 +31,10 @@ public enum AllowReason: String, Codable { case adClickAttribution case otherThirdPartyRequest } - + // Populated with relevant info at the point of detection. public struct DetectedRequest: Encodable { - + public let url: String public let eTLDplus1: String? public let state: BlockingState @@ -44,7 +43,7 @@ public struct DetectedRequest: Encodable { public let category: String? public let prevalence: Double? public let pageUrl: String - + public init(url: String, eTLDplus1: String?, knownTracker: KnownTracker?, entity: Entity?, state: BlockingState, pageUrl: String) { self.url = url self.eTLDplus1 = eTLDplus1 @@ -60,11 +59,11 @@ public struct DetectedRequest: Encodable { guard let escapedStringURL = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return nil } return URL(string: escapedStringURL)?.host } - + public var networkNameForDisplay: String { entityName ?? eTLDplus1 ?? url } - + public var isBlocked: Bool { state == .blocked } @@ -88,7 +87,7 @@ extension DetectedRequest: Hashable, Equatable { } extension BlockingState: Equatable, Hashable { - + public static func == (lhs: BlockingState, rhs: BlockingState) -> Bool { switch (lhs, rhs) { case (.blocked, .blocked): @@ -99,5 +98,5 @@ extension BlockingState: Equatable, Hashable { return false } } - + } diff --git a/Sources/Crashes/CrashCollection.swift b/Sources/Crashes/CrashCollection.swift index 9fa65323b..8c6ca4ac6 100644 --- a/Sources/Crashes/CrashCollection.swift +++ b/Sources/Crashes/CrashCollection.swift @@ -31,7 +31,7 @@ public struct CrashCollection { MXMetricManager.shared.add(collector) } - class CrashCollector: NSObject, MXMetricManagerSubscriber { + final class CrashCollector: NSObject, MXMetricManagerSubscriber { var completion: ([String: String]) -> Void = { _ in } diff --git a/Sources/DDGSync/DDGSyncing.swift b/Sources/DDGSync/DDGSyncing.swift index bd0f619e2..76869d90c 100644 --- a/Sources/DDGSync/DDGSyncing.swift +++ b/Sources/DDGSync/DDGSyncing.swift @@ -97,7 +97,7 @@ public protocol DDGSyncing: DDGSyncingDebuggingSupport { * Generate secure keys * Call /signup endpoint * Store Primary Key, Secret Key, User Id and JWT token - + Notes: * The primary key in combination with the user id, is the recovery code. This can be used to (re)connect devices. * The JWT token contains the authorisation required to call an endpoint. If a device is removed from sync the token will be invalidated on the server and subsequent calls will fail. diff --git a/Sources/DDGSync/DataProvider.swift b/Sources/DDGSync/DataProvider.swift index 3fca66c76..e733851d3 100644 --- a/Sources/DDGSync/DataProvider.swift +++ b/Sources/DDGSync/DataProvider.swift @@ -1,6 +1,5 @@ // // DataProvider.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/DDGSync/RecoveryPDFGenerator.swift b/Sources/DDGSync/RecoveryPDFGenerator.swift index ca165a98d..a7120ac0b 100644 --- a/Sources/DDGSync/RecoveryPDFGenerator.swift +++ b/Sources/DDGSync/RecoveryPDFGenerator.swift @@ -101,7 +101,7 @@ public struct RecoveryPDFGenerator { func qrcode(_ text: String, size: Int) -> CGImage { let data = Data(text.utf8) - let qrCodeFilter: CIFilter = CIFilter.init(name: "CIQRCodeGenerator")! + let qrCodeFilter: CIFilter = CIFilter(name: "CIQRCodeGenerator")! qrCodeFilter.setValue(data, forKey: "inputMessage") qrCodeFilter.setValue("H", forKey: "inputCorrectionLevel") diff --git a/Sources/DDGSync/SyncFeatureEntity.swift b/Sources/DDGSync/SyncFeatureEntity.swift index 34f3d07b9..896ce1bfb 100644 --- a/Sources/DDGSync/SyncFeatureEntity.swift +++ b/Sources/DDGSync/SyncFeatureEntity.swift @@ -1,6 +1,5 @@ // // SyncFeatureEntity.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/DDGSync/SyncModels.swift b/Sources/DDGSync/SyncModels.swift index 4156031cb..88215a5e0 100644 --- a/Sources/DDGSync/SyncModels.swift +++ b/Sources/DDGSync/SyncModels.swift @@ -1,6 +1,5 @@ // -// File.swift -// DuckDuckGo +// SyncModels.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/DDGSync/SyncableSettingsMetadata.swift b/Sources/DDGSync/SyncableSettingsMetadata.swift index 62e568724..d762d47c2 100644 --- a/Sources/DDGSync/SyncableSettingsMetadata.swift +++ b/Sources/DDGSync/SyncableSettingsMetadata.swift @@ -1,6 +1,5 @@ // // SyncableSettingsMetadata.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/DDGSync/internal/BookmarkUpdate.swift b/Sources/DDGSync/internal/BookmarkUpdate.swift index 7d28767c8..d874a945c 100644 --- a/Sources/DDGSync/internal/BookmarkUpdate.swift +++ b/Sources/DDGSync/internal/BookmarkUpdate.swift @@ -19,7 +19,7 @@ import Foundation struct BookmarkUpdate: Codable { - + let id: String? let next: String? let parent: String? @@ -30,15 +30,15 @@ struct BookmarkUpdate: Codable { let folder: Folder? let deleted: String? - + struct Page: Codable { let url: String? } - + struct Favorite: Codable { let next: String? } - + struct Folder: Codable { } } diff --git a/Sources/DDGSync/internal/Crypter.swift b/Sources/DDGSync/internal/Crypter.swift index 47741e926..fff7ec7ff 100644 --- a/Sources/DDGSync/internal/Crypter.swift +++ b/Sources/DDGSync/internal/Crypter.swift @@ -95,7 +95,7 @@ struct Crypter: CryptingInternal { func extractLoginInfo(recoveryKey: SyncCode.RecoveryKey) throws -> ExtractedLoginInfo { let primaryKeySize = Int(DDGSYNCCRYPTO_PRIMARY_KEY_SIZE.rawValue) - + var primaryKeyBytes = [UInt8](repeating: 0, count: primaryKeySize) var passwordHashBytes = [UInt8](repeating: 0, count: Int(DDGSYNCCRYPTO_HASH_SIZE.rawValue)) var strechedPrimaryKeyBytes = [UInt8](repeating: 0, count: Int(DDGSYNCCRYPTO_STRETCHED_PRIMARY_KEY_SIZE.rawValue)) @@ -106,7 +106,7 @@ struct Crypter: CryptingInternal { guard DDGSYNCCRYPTO_OK == result else { throw SyncError.failedToCreateAccountKeys("ddgSyncPrepareForLogin failed: \(result)") } - + return ExtractedLoginInfo( userId: recoveryKey.userId, primaryKey: Data(primaryKeyBytes), diff --git a/Sources/DDGSync/internal/Endpoints.swift b/Sources/DDGSync/internal/Endpoints.swift index 161394dad..1985314bf 100644 --- a/Sources/DDGSync/internal/Endpoints.swift +++ b/Sources/DDGSync/internal/Endpoints.swift @@ -18,7 +18,7 @@ import Foundation -class Endpoints { +final class Endpoints { private(set) var baseURL: URL diff --git a/Sources/DDGSync/internal/KeyValueStore.swift b/Sources/DDGSync/internal/KeyValueStore.swift index 1d108d99d..5100945b6 100644 --- a/Sources/DDGSync/internal/KeyValueStore.swift +++ b/Sources/DDGSync/internal/KeyValueStore.swift @@ -1,5 +1,5 @@ // -// SecureStorage.swift +// KeyValueStore.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/DDGSync/internal/ProductionDependencies.swift b/Sources/DDGSync/internal/ProductionDependencies.swift index 06682ac8e..c40bfa019 100644 --- a/Sources/DDGSync/internal/ProductionDependencies.swift +++ b/Sources/DDGSync/internal/ProductionDependencies.swift @@ -37,7 +37,7 @@ struct ProductionDependencies: SyncDependencies { private let getLog: () -> OSLog init(serverEnvironment: ServerEnvironment, errorEvents: EventMapping, log: @escaping @autoclosure () -> OSLog = .disabled) { - + self.init(fileStorageUrl: FileManager.default.applicationSupportDirectoryForComponent(named: "Sync"), serverEnvironment: serverEnvironment, keyValueStore: KeyValueStore(), @@ -45,7 +45,7 @@ struct ProductionDependencies: SyncDependencies { errorEvents: errorEvents, log: log()) } - + init( fileStorageUrl: URL, serverEnvironment: ServerEnvironment, diff --git a/Sources/DDGSync/internal/RecoveryKeyTransmitter.swift b/Sources/DDGSync/internal/RecoveryKeyTransmitter.swift index bc8a2719a..a05de4615 100644 --- a/Sources/DDGSync/internal/RecoveryKeyTransmitter.swift +++ b/Sources/DDGSync/internal/RecoveryKeyTransmitter.swift @@ -1,6 +1,5 @@ // // RecoveryKeyTransmitter.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift b/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift index 3149d5339..e67c1e7f5 100644 --- a/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift +++ b/Sources/DDGSync/internal/RemoteAPIRequestCreatingExtensions.swift @@ -1,6 +1,5 @@ // -// RemoteAPIRequestCreating.swift -// DuckDuckGo +// RemoteAPIRequestCreatingExtensions.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/DDGSync/internal/RemoteAPIRequestCreator.swift b/Sources/DDGSync/internal/RemoteAPIRequestCreator.swift index 1ed3b51d2..c0cf85b45 100644 --- a/Sources/DDGSync/internal/RemoteAPIRequestCreator.swift +++ b/Sources/DDGSync/internal/RemoteAPIRequestCreator.swift @@ -1,5 +1,5 @@ // -// RemoteAPIRequestCreating.swift +// RemoteAPIRequestCreator.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/DDGSync/internal/RemoteConnector.swift b/Sources/DDGSync/internal/RemoteConnector.swift index f9c729a59..cc00c85e8 100644 --- a/Sources/DDGSync/internal/RemoteConnector.swift +++ b/Sources/DDGSync/internal/RemoteConnector.swift @@ -1,5 +1,5 @@ // -// RemoteAPIRequestCreating.swift +// RemoteConnector.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -18,8 +18,8 @@ import Foundation -class RemoteConnector: RemoteConnecting { - +final class RemoteConnector: RemoteConnecting { + let code: String let connectInfo: ConnectInfo diff --git a/Sources/DDGSync/internal/SecureStorage.swift b/Sources/DDGSync/internal/SecureStorage.swift index 361be331d..74871c650 100644 --- a/Sources/DDGSync/internal/SecureStorage.swift +++ b/Sources/DDGSync/internal/SecureStorage.swift @@ -23,17 +23,17 @@ struct SecureStorage: SecureStoring { // DO NOT CHANGE except if you want to deliberately invalidate all users's sync accounts. // The keys have a uid to deter casual hacker from easily seeing which keychain entry is related to what. private static let encodedKey = "833CC26A-3804-4D37-A82A-C245BC670692".data(using: .utf8) - + private static let defaultQuery: [AnyHashable: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: "\(Bundle.main.bundleIdentifier ?? "com.duckduckgo").sync", kSecAttrGeneric: encodedKey as Any, kSecAttrAccount: encodedKey as Any ] - + func persistAccount(_ account: SyncAccount) throws { let data = try JSONEncoder.snakeCaseKeys.encode(account) - + var query = Self.defaultQuery query[kSecUseDataProtectionKeychain] = true query[kSecAttrAccessible] = kSecAttrAccessibleWhenUnlocked @@ -63,11 +63,11 @@ struct SecureStorage: SecureStoring { guard [errSecSuccess, errSecItemNotFound].contains(status) else { throw SyncError.failedToReadSecureStore(status: status) } - + if let data = item as? Data { return try JSONDecoder.snakeCaseKeys.decode(SyncAccount.self, from: data) } - + return nil } @@ -77,5 +77,5 @@ struct SecureStorage: SecureStoring { throw SyncError.failedToRemoveSecureStore(status: status) } } - + } diff --git a/Sources/DDGSync/internal/SyncOperation.swift b/Sources/DDGSync/internal/SyncOperation.swift index e702f687f..498a48624 100644 --- a/Sources/DDGSync/internal/SyncOperation.swift +++ b/Sources/DDGSync/internal/SyncOperation.swift @@ -1,6 +1,5 @@ // // SyncOperation.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,7 +20,7 @@ import Foundation import Combine import Common -class SyncOperation: Operation { +final class SyncOperation: Operation { let dataProviders: [DataProviding] let storage: SecureStoring diff --git a/Sources/DDGSync/internal/SyncQueue.swift b/Sources/DDGSync/internal/SyncQueue.swift index 889c758bf..9230a426b 100644 --- a/Sources/DDGSync/internal/SyncQueue.swift +++ b/Sources/DDGSync/internal/SyncQueue.swift @@ -1,6 +1,5 @@ // // SyncQueue.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -30,9 +29,9 @@ struct SyncOperationError: Error { let perFeatureErrors: [Feature: Error] init(featureErrors: [FeatureError]) { - perFeatureErrors = featureErrors.reduce(into: .init(), { partialResult, featureError in + perFeatureErrors = featureErrors.reduce(into: .init()) { partialResult, featureError in partialResult[featureError.feature] = featureError.underlyingError - }) + } } } @@ -53,7 +52,7 @@ struct SyncResult { } } -class SyncQueue { +final class SyncQueue { let dataProviders: [DataProviding] let storage: SecureStoring diff --git a/Sources/DDGSync/internal/SyncRequestMaker.swift b/Sources/DDGSync/internal/SyncRequestMaker.swift index 57bd507c3..12382e667 100644 --- a/Sources/DDGSync/internal/SyncRequestMaker.swift +++ b/Sources/DDGSync/internal/SyncRequestMaker.swift @@ -1,6 +1,5 @@ // // SyncRequestMaker.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -26,7 +25,7 @@ protocol SyncRequestMaking { struct SyncRequestMaker: SyncRequestMaking { let storage: SecureStoring - let api: RemoteAPIRequestCreating + let api: RemoteAPIRequestCreating let endpoints: Endpoints let dateFormatter = ISO8601DateFormatter() diff --git a/Sources/DDGSync/internal/SyncScheduler.swift b/Sources/DDGSync/internal/SyncScheduler.swift index 0d5c28a06..b99e016a8 100644 --- a/Sources/DDGSync/internal/SyncScheduler.swift +++ b/Sources/DDGSync/internal/SyncScheduler.swift @@ -1,6 +1,5 @@ // // SyncScheduler.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -34,7 +33,7 @@ protocol SchedulingInternal: AnyObject, Scheduling { var resumeSyncPublisher: AnyPublisher { get } } -class SyncScheduler: SchedulingInternal { +final class SyncScheduler: SchedulingInternal { func notifyDataChanged() { if isEnabled { syncTriggerSubject.send() diff --git a/Sources/Navigation/AuthChallengeDisposition.swift b/Sources/Navigation/AuthChallengeDisposition.swift index 30f5926e6..059c62b1f 100644 --- a/Sources/Navigation/AuthChallengeDisposition.swift +++ b/Sources/Navigation/AuthChallengeDisposition.swift @@ -48,7 +48,7 @@ public enum AuthChallengeDisposition: Sendable { return "rejectProtectionSpace" } } - + } extension AuthChallengeDisposition? { diff --git a/Sources/Navigation/DistributedNavigationDelegate.swift b/Sources/Navigation/DistributedNavigationDelegate.swift index 1966393e8..3f94ac3c0 100644 --- a/Sources/Navigation/DistributedNavigationDelegate.swift +++ b/Sources/Navigation/DistributedNavigationDelegate.swift @@ -21,8 +21,6 @@ import Common import Foundation import WebKit -// swiftlint:disable file_length -// swiftlint:disable line_length public final class DistributedNavigationDelegate: NSObject { internal var responders = ResponderChain() @@ -951,7 +949,7 @@ extension DistributedNavigationDelegate { assert((handler.ref.responder as? NSObject)!.responds(to: selector)) customDelegateMethodHandlers[selector] = handler.ref } - + public func registerCustomDelegateMethodHandler(_ handler: ResponderRefMaker, forSelectorsNamed selectors: [String]) { for selector in selectors { registerCustomDelegateMethodHandler(handler, forSelectorNamed: selector) @@ -970,6 +968,3 @@ extension DistributedNavigationDelegate { } } - -// swiftlint:enable line_length -// swiftlint:enable file_length diff --git a/Sources/Navigation/Extensions/WKErrorExtension.swift b/Sources/Navigation/Extensions/WKErrorExtension.swift index 540593fc5..f1a5c238d 100644 --- a/Sources/Navigation/Extensions/WKErrorExtension.swift +++ b/Sources/Navigation/Extensions/WKErrorExtension.swift @@ -35,23 +35,9 @@ extension WKError { } -private protocol WKErrorProtocol { - static var _WebKitErrorDomain: String { get } -} - -extension WKError: WKErrorProtocol { +extension WKError { - // suppress WebKit.WebKitErrorDomain deprecation warning - @available(macOS, introduced: 10.3, deprecated: 10.14) - fileprivate static var _WebKitErrorDomain: String { -#if os(macOS) - assert(WebKit.WebKitErrorDomain == "WebKitErrorDomain") - return WebKit.WebKitErrorDomain -#else - return "WebKitErrorDomain" -#endif - } - static var WebKitErrorDomain: String { (self as WKErrorProtocol.Type)._WebKitErrorDomain } + static let WebKitErrorDomain = "WebKitErrorDomain" } diff --git a/Sources/Navigation/Extensions/WKFrameInfoExtension.swift b/Sources/Navigation/Extensions/WKFrameInfoExtension.swift index 88ebd3be5..b2badbff0 100644 --- a/Sources/Navigation/Extensions/WKFrameInfoExtension.swift +++ b/Sources/Navigation/Extensions/WKFrameInfoExtension.swift @@ -72,7 +72,7 @@ public extension WKFrameInfo { breakByRaisingSigInt("Don‘t use `WKFrameInfo.request` as it has incorrect nullability\n" + "Use `WKFrameInfo.safeRequest` instead") } - + return self.swizzledRequest() // call the original } diff --git a/Sources/Navigation/Extensions/WKNavigationResponseExtension.swift b/Sources/Navigation/Extensions/WKNavigationResponseExtension.swift index 14bff5572..d60939f7c 100644 --- a/Sources/Navigation/Extensions/WKNavigationResponseExtension.swift +++ b/Sources/Navigation/Extensions/WKNavigationResponseExtension.swift @@ -1,5 +1,5 @@ // -// WKNavigationActionExtension.swift +// WKNavigationResponseExtension.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/Navigation/NavigationResponse.swift b/Sources/Navigation/NavigationResponse.swift index c5f503324..c82e86bae 100644 --- a/Sources/Navigation/NavigationResponse.swift +++ b/Sources/Navigation/NavigationResponse.swift @@ -70,7 +70,7 @@ public extension NavigationResponse { extension NavigationResponse: CustomDebugStringConvertible { public var debugDescription: String { - let statusCode = self.httpStatusCode.map { String.init($0) } ?? "-" + let statusCode = self.httpStatusCode.map { String($0) } ?? "-" return "" } } diff --git a/Sources/Navigation/NavigationType.swift b/Sources/Navigation/NavigationType.swift index ea324233f..ae4773ed1 100644 --- a/Sources/Navigation/NavigationType.swift +++ b/Sources/Navigation/NavigationType.swift @@ -105,7 +105,7 @@ public extension NavigationType { if case .redirect = self { return true } return false } - + var redirect: RedirectType? { if case .redirect(let redirect) = self { return redirect } return nil diff --git a/Sources/NetworkProtection/Controllers/TunnelController.swift b/Sources/NetworkProtection/Controllers/TunnelController.swift index d95f1348c..ea8834a9c 100644 --- a/Sources/NetworkProtection/Controllers/TunnelController.swift +++ b/Sources/NetworkProtection/Controllers/TunnelController.swift @@ -19,7 +19,7 @@ import Foundation /// This protocol offers an interface to control the tunnel. -/// +/// public protocol TunnelController { // MARK: - Starting & Stopping the VPN @@ -33,6 +33,6 @@ public protocol TunnelController { func stop() async /// Whether the tunnel is connected - /// + /// var isConnected: Bool { get async } } diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift index 11856ca30..508afb3cd 100644 --- a/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift +++ b/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift @@ -1,6 +1,5 @@ // // NetworkProtectionError.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyMonitor.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyMonitor.swift index 7a582dfac..3271c447f 100644 --- a/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyMonitor.swift +++ b/Sources/NetworkProtection/Diagnostics/NetworkProtectionLatencyMonitor.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionTunnelFailureMonitor.swift +// NetworkProtectionLatencyMonitor.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -149,7 +149,7 @@ final public class NetworkProtectionLatencyMonitor { } else { self?.subject.send(.error) } - + os_log("⚫️ Average: %{public}f milliseconds", log: .networkProtectionPixel, type: .debug, measurements.average) return measurements @@ -157,7 +157,7 @@ final public class NetworkProtectionLatencyMonitor { .map { ConnectionQuality(average: $0.average) } .sink { [weak self] quality in let now = Date() - if let self, + if let self, (now.timeIntervalSince1970 - self.lastLatencyReported.timeIntervalSince1970 >= Self.reportThreshold) || ignoreThreshold { self.subject.send(.quality(quality)) self.lastLatencyReported = now diff --git a/Sources/NetworkProtection/Diagnostics/WireGuardAdapterError+NetworkProtectionErrorConvertible.swift b/Sources/NetworkProtection/Diagnostics/WireGuardAdapterError+NetworkProtectionErrorConvertible.swift index d23d13e3e..ae8b5486a 100644 --- a/Sources/NetworkProtection/Diagnostics/WireGuardAdapterError+NetworkProtectionErrorConvertible.swift +++ b/Sources/NetworkProtection/Diagnostics/WireGuardAdapterError+NetworkProtectionErrorConvertible.swift @@ -1,6 +1,5 @@ // -// WireguardAdapterError+NetworkProtectionErrorConvertible.swift -// DuckDuckGo +// WireGuardAdapterError+NetworkProtectionErrorConvertible.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift index 57b478cfa..c2a5ae7cd 100644 --- a/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift +++ b/Sources/NetworkProtection/ExtensionMessage/ExtensionMessage.swift @@ -133,7 +133,7 @@ public enum ExtensionMessage: RawRepresentable { case .simulateConnectionInterruption: self = .simulateConnectionInterruption - + case .none: assertionFailure("Invalid data") return nil diff --git a/Sources/NetworkProtection/KeyManagement/KeyPair.swift b/Sources/NetworkProtection/KeyManagement/KeyPair.swift index fefcebab6..62ceabf1b 100644 --- a/Sources/NetworkProtection/KeyManagement/KeyPair.swift +++ b/Sources/NetworkProtection/KeyManagement/KeyPair.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionKeyStore.swift +// KeyPair.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/KeyManagement/NetworkProtectionKeychainStore.swift b/Sources/NetworkProtection/KeyManagement/NetworkProtectionKeychainStore.swift index 659cb691e..b28996c26 100644 --- a/Sources/NetworkProtection/KeyManagement/NetworkProtectionKeychainStore.swift +++ b/Sources/NetworkProtection/KeyManagement/NetworkProtectionKeychainStore.swift @@ -44,7 +44,7 @@ final class NetworkProtectionKeychainStore { init(label: String, serviceName: String, keychainType: KeychainType) { - + self.label = label self.serviceName = serviceName self.keychainType = keychainType diff --git a/Sources/NetworkProtection/Models/AnyIPAddress.swift b/Sources/NetworkProtection/Models/AnyIPAddress.swift index c435f8fec..6677e0833 100644 --- a/Sources/NetworkProtection/Models/AnyIPAddress.swift +++ b/Sources/NetworkProtection/Models/AnyIPAddress.swift @@ -131,7 +131,7 @@ extension AnyIPAddress: Codable { public init(from decoder: Decoder) throws { let string = try decoder.singleValueContainer().decode(String.self) - guard let address = Self.init(string) else { + guard let address = Self(string) else { throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Could not decode IP from \(string)", underlyingError: nil)) diff --git a/Sources/NetworkProtection/Models/NetworkProtectionLocation.swift b/Sources/NetworkProtection/Models/NetworkProtectionLocation.swift index 6317d1ea9..7f38da2ef 100644 --- a/Sources/NetworkProtection/Models/NetworkProtectionLocation.swift +++ b/Sources/NetworkProtection/Models/NetworkProtectionLocation.swift @@ -1,6 +1,5 @@ // // NetworkProtectionLocation.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/Networking/NWConnectionExtension.swift b/Sources/NetworkProtection/Networking/NWConnectionExtension.swift index 1b260e4db..e3d3ed867 100644 --- a/Sources/NetworkProtection/Networking/NWConnectionExtension.swift +++ b/Sources/NetworkProtection/Networking/NWConnectionExtension.swift @@ -24,7 +24,7 @@ extension NWConnection { var stateUpdateStream: AsyncStream { let (stream, continuation) = AsyncStream.makeStream(of: State.self) - class ConnectionLifeTimeTracker { + final class ConnectionLifeTimeTracker { let continuation: AsyncStream.Continuation init(continuation: AsyncStream.Continuation) { self.continuation = continuation diff --git a/Sources/NetworkProtection/Networking/Pinger.swift b/Sources/NetworkProtection/Networking/Pinger.swift index 743851029..bdb70981e 100644 --- a/Sources/NetworkProtection/Networking/Pinger.swift +++ b/Sources/NetworkProtection/Networking/Pinger.swift @@ -16,7 +16,7 @@ // limitations under the License. // -// swiftlint:disable file_length identifier_name +// swiftlint:disable identifier_name import Darwin import Foundation @@ -493,7 +493,9 @@ struct Socket { func setopt(_ level: Int32, _ opt: Int32, value: T) { var value = value - let result = setsockopt(socket, level, opt, &value, socklen_t(MemoryLayout.size)) + let result = withUnsafePointer(to: &value) { valuePtr in + setsockopt(socket, level, opt, valuePtr, socklen_t(MemoryLayout.size)) + } assert(result == 0) } @@ -626,4 +628,4 @@ struct Socket { } -// swiftlint:enable file_length identifier_name +// swiftlint:enable identifier_name diff --git a/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift b/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift index 12108a1e8..359c318d5 100644 --- a/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift +++ b/Sources/NetworkProtection/Notifications/NetworkProtectionNotification.swift @@ -1,5 +1,5 @@ // -// DistributedNotification.swift +// NetworkProtectionNotification.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index f2634d486..ad85bb378 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -25,7 +25,7 @@ import Foundation import NetworkExtension import UserNotifications -// swiftlint:disable file_length type_body_length line_length +// swiftlint:disable:next type_body_length open class PacketTunnelProvider: NEPacketTunnelProvider { public enum Event { @@ -125,7 +125,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } } - public let lastSelectedServerInfoPublisher = CurrentValueSubject.init(nil) + public let lastSelectedServerInfoPublisher = CurrentValueSubject(nil) private var includedRoutes: [IPAddressRange]? @@ -255,9 +255,9 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } }() - public lazy var tunnelFailureMonitor = NetworkProtectionTunnelFailureMonitor(tunnelProvider: self, - timerQueue: timerQueue, - log: .networkProtectionPixel) + public lazy var tunnelFailureMonitor = NetworkProtectionTunnelFailureMonitor(tunnelProvider: self, + timerQueue: timerQueue, + log: .networkProtectionPixel) public lazy var latencyMonitor = NetworkProtectionLatencyMonitor(serverIP: { [weak self] in self?.lastSelectedServerInfo?.ipv4 }, timerQueue: timerQueue, @@ -581,7 +581,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { } private func startTunnel(with tunnelConfiguration: TunnelConfiguration, onDemand: Bool, completionHandler: @escaping (Error?) -> Void) { - + adapter.start(tunnelConfiguration: tunnelConfiguration) { [weak self] error in if let error { os_log("🔵 Starting tunnel failed with %{public}@", log: .networkProtection, type: .error, error.localizedDescription) @@ -1144,4 +1144,3 @@ extension WireGuardAdapterError: LocalizedError, CustomDebugStringConvertible { } } -// swiftlint:enable file_length type_body_length line_length diff --git a/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift b/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift index 207fe5020..c4ac37443 100644 --- a/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift +++ b/Sources/NetworkProtection/Repositories/NetworkProtectionLocationListRepository.swift @@ -29,7 +29,7 @@ final public class NetworkProtectionLocationListCompositeRepository: NetworkProt private let tokenStore: NetworkProtectionTokenStore private let errorEvents: EventMapping - convenience public init(environment: VPNSettings.SelectedEnvironment, + convenience public init(environment: VPNSettings.SelectedEnvironment, tokenStore: NetworkProtectionTokenStore, errorEvents: EventMapping) { self.init( @@ -39,7 +39,7 @@ final public class NetworkProtectionLocationListCompositeRepository: NetworkProt ) } - init(client: NetworkProtectionClient, + init(client: NetworkProtectionClient, tokenStore: NetworkProtectionTokenStore, errorEvents: EventMapping) { self.client = client diff --git a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+vpnFirstEnabled.swift b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+vpnFirstEnabled.swift index dbf9b1f30..0dbd4f4c1 100644 --- a/Sources/NetworkProtection/Settings/Extensions/UserDefaults+vpnFirstEnabled.swift +++ b/Sources/NetworkProtection/Settings/Extensions/UserDefaults+vpnFirstEnabled.swift @@ -1,5 +1,5 @@ // -// UserDefaults+showInMenuBar.swift +// UserDefaults+vpnFirstEnabled.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/Settings/VPNSettings.swift b/Sources/NetworkProtection/Settings/VPNSettings.swift index e9c7fb3db..8e7411190 100644 --- a/Sources/NetworkProtection/Settings/VPNSettings.swift +++ b/Sources/NetworkProtection/Settings/VPNSettings.swift @@ -19,8 +19,6 @@ import Combine import Foundation -// swiftlint:disable type_body_length file_length - /// Persists and publishes changes to tunnel settings. /// /// It's strongly recommended to use shared `UserDefaults` to initialize this class, as `VPNSettings` @@ -426,5 +424,3 @@ public final class VPNSettings { } } } - -// swiftlint:enable type_body_length file_length diff --git a/Sources/NetworkProtection/Status/ConnectionErrorObserver/ConnectionErrorObserverThroughSession.swift b/Sources/NetworkProtection/Status/ConnectionErrorObserver/ConnectionErrorObserverThroughSession.swift index ced2281e9..52dcbc348 100644 --- a/Sources/NetworkProtection/Status/ConnectionErrorObserver/ConnectionErrorObserverThroughSession.swift +++ b/Sources/NetworkProtection/Status/ConnectionErrorObserver/ConnectionErrorObserverThroughSession.swift @@ -1,5 +1,5 @@ // -// ConnectionErrorObserver.swift +// ConnectionErrorObserverThroughSession.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/Status/ConnectionServerInfoObserver/ConnectionServerInfoObserver.swift b/Sources/NetworkProtection/Status/ConnectionServerInfoObserver/ConnectionServerInfoObserver.swift index a540b64ba..770b168c7 100644 --- a/Sources/NetworkProtection/Status/ConnectionServerInfoObserver/ConnectionServerInfoObserver.swift +++ b/Sources/NetworkProtection/Status/ConnectionServerInfoObserver/ConnectionServerInfoObserver.swift @@ -1,5 +1,5 @@ // -// ConnectionStatusObserver.swift +// ConnectionServerInfoObserver.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/Status/ConnectionServerInfoObserver/ConnectionServerInfoObserverThroughSession.swift b/Sources/NetworkProtection/Status/ConnectionServerInfoObserver/ConnectionServerInfoObserverThroughSession.swift index a445a24e8..8bd716219 100644 --- a/Sources/NetworkProtection/Status/ConnectionServerInfoObserver/ConnectionServerInfoObserverThroughSession.swift +++ b/Sources/NetworkProtection/Status/ConnectionServerInfoObserver/ConnectionServerInfoObserverThroughSession.swift @@ -32,7 +32,7 @@ public class ConnectionServerInfoObserverThroughSession: ConnectionServerInfoObs } private let subject = CurrentValueSubject(.unknown) - + // MARK: - Notifications private let notificationCenter: NotificationCenter diff --git a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift index 8a1b06050..1fe81d07d 100644 --- a/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift +++ b/Sources/NetworkProtection/Status/UserNotifications/NetworkProtectionNotificationsPresenterTogglableDecorator.swift @@ -1,6 +1,5 @@ // // NetworkProtectionNotificationsPresenterTogglableDecorator.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionSelectedServerStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionSelectedServerStore.swift index d93197465..f87ade666 100644 --- a/Sources/NetworkProtection/Storage/NetworkProtectionSelectedServerStore.swift +++ b/Sources/NetworkProtection/Storage/NetworkProtectionSelectedServerStore.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionServerSelection.swift +// NetworkProtectionSelectedServerStore.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -19,7 +19,7 @@ import Foundation /* protocol NetworkProtectionSelectedServerStore: AnyObject { - + var selectedServer: SelectedNetworkProtectionServer { get set } func reset() } diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionServerListStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionServerListStore.swift index dda442fe0..e3f36833f 100644 --- a/Sources/NetworkProtection/Storage/NetworkProtectionServerListStore.swift +++ b/Sources/NetworkProtection/Storage/NetworkProtectionServerListStore.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionServer.swift +// NetworkProtectionServerListStore.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionSimulationOptionsStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionSimulationOptionsStore.swift index 498638aaf..ccb56a530 100644 --- a/Sources/NetworkProtection/Storage/NetworkProtectionSimulationOptionsStore.swift +++ b/Sources/NetworkProtection/Storage/NetworkProtectionSimulationOptionsStore.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionSimulationOption.swift +// NetworkProtectionSimulationOptionsStore.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionTunnelHealthStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionTunnelHealthStore.swift index 10e334afb..9a0badd35 100644 --- a/Sources/NetworkProtection/Storage/NetworkProtectionTunnelHealthStore.swift +++ b/Sources/NetworkProtection/Storage/NetworkProtectionTunnelHealthStore.swift @@ -57,7 +57,7 @@ public final class NetworkProtectionTunnelHealthStore { get { userDefaults.bool(forKey: Self.isHavingConnectivityIssuesKey) } - + set { guard newValue != userDefaults.bool(forKey: Self.isHavingConnectivityIssuesKey) else { return diff --git a/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift b/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift index c25b70578..6da060ae1 100644 --- a/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift +++ b/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift @@ -7,8 +7,6 @@ import NetworkExtension import WireGuard import Common -// swiftlint:disable file_length - public enum WireGuardAdapterError: Error { /// Failure to locate tunnel file descriptor. case cannotLocateTunnelFileDescriptor @@ -38,7 +36,6 @@ private enum State { case temporaryShutdown(_ settingsGenerator: PacketTunnelSettingsGenerator) } -// swiftlint:disable:next type_body_length public class WireGuardAdapter { public typealias LogHandler = (WireGuardLogLevel, String) -> Void @@ -228,14 +225,14 @@ public class WireGuardAdapter { continuation.resume(throwing: GetBytesTransmittedError.couldNotObtainAdapterConfiguration) return } - + var numberOfSeconds = UInt64(0) let lines = configuration.components(separatedBy: .newlines) for line in lines where line.hasPrefix(ConfigurationFields.mostRecentHandshake.configLinePrefix) { numberOfSeconds = UInt64(line.dropFirst(ConfigurationFields.mostRecentHandshake.configLinePrefix.count)) ?? 0 break } - + continuation.resume(returning: TimeInterval(numberOfSeconds)) } } @@ -582,5 +579,3 @@ private extension Network.NWPath.Status { } } } - -// swiftlint:enable file_length diff --git a/Sources/NetworkProtectionTestUtils/Controllers/MockTunnelController.swift b/Sources/NetworkProtectionTestUtils/Controllers/MockTunnelController.swift index bb36af2a2..44a12cae6 100644 --- a/Sources/NetworkProtectionTestUtils/Controllers/MockTunnelController.swift +++ b/Sources/NetworkProtectionTestUtils/Controllers/MockTunnelController.swift @@ -1,6 +1,5 @@ // // MockTunnelController.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtectionTestUtils/FeatureActivation/NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift b/Sources/NetworkProtectionTestUtils/FeatureActivation/NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift index 7e52003b9..8642e7628 100644 --- a/Sources/NetworkProtectionTestUtils/FeatureActivation/NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift +++ b/Sources/NetworkProtectionTestUtils/FeatureActivation/NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift @@ -1,6 +1,5 @@ // -// NetworkProtectionRedemptionCoordinatorTestExtensions.swift -// DuckDuckGo +// NetworkProtectionCodeRedemptionCoordinatorTestExtensions.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/NetworkProtectionTestUtils/KeyManagement/MockNetworkProtectionTokenStore.swift b/Sources/NetworkProtectionTestUtils/KeyManagement/MockNetworkProtectionTokenStore.swift index e313ed9ba..673ee410a 100644 --- a/Sources/NetworkProtectionTestUtils/KeyManagement/MockNetworkProtectionTokenStore.swift +++ b/Sources/NetworkProtectionTestUtils/KeyManagement/MockNetworkProtectionTokenStore.swift @@ -1,6 +1,5 @@ // -// MockNetworkProtectionTokenStorage.swift -// DuckDuckGo +// MockNetworkProtectionTokenStore.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -23,7 +22,7 @@ import NetworkProtection public final class MockNetworkProtectionTokenStorage: NetworkProtectionTokenStore { public init() { - + } var spyToken: String? diff --git a/Sources/NetworkProtectionTestUtils/Networking/MockNetworkProtectionClient.swift b/Sources/NetworkProtectionTestUtils/Networking/MockNetworkProtectionClient.swift index a4d4a840e..a845e1a8b 100644 --- a/Sources/NetworkProtectionTestUtils/Networking/MockNetworkProtectionClient.swift +++ b/Sources/NetworkProtectionTestUtils/Networking/MockNetworkProtectionClient.swift @@ -1,6 +1,5 @@ // // MockNetworkProtectionClient.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -35,7 +34,7 @@ public final class MockNetworkProtectionClient: NetworkProtectionClient { spyGetLocationsAuthToken = authToken return stubGetLocations } - + public var spyRedeemInviteCode: String? public var stubRedeem: Result = .success("") public var redeemCalled: Bool { diff --git a/Sources/NetworkProtectionTestUtils/Status/MockNetworkProtectionStatusReporter.swift b/Sources/NetworkProtectionTestUtils/Status/MockNetworkProtectionStatusReporter.swift index c17dc19be..b8487ccad 100644 --- a/Sources/NetworkProtectionTestUtils/Status/MockNetworkProtectionStatusReporter.swift +++ b/Sources/NetworkProtectionTestUtils/Status/MockNetworkProtectionStatusReporter.swift @@ -1,5 +1,5 @@ // -// NetworkProtectionStatusReporter.swift +// MockNetworkProtectionStatusReporter.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -54,7 +54,7 @@ public final class MockNetworkProtectionStatusReporter: NetworkProtectionStatusR // MARK: - Forcing Refreshes public func forceRefresh() { - + } } diff --git a/Sources/Networking/APIHeaders.swift b/Sources/Networking/APIHeaders.swift index 33160e3b3..8cae4962b 100644 --- a/Sources/Networking/APIHeaders.swift +++ b/Sources/Networking/APIHeaders.swift @@ -1,6 +1,5 @@ // // APIHeaders.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,15 +21,15 @@ import Foundation public typealias HTTPHeaders = [String: String] public extension APIRequest { - + struct Headers { - + public typealias UserAgent = String private static var userAgent: UserAgent? public static func setUserAgent(_ userAgent: UserAgent) { self.userAgent = userAgent } - + let userAgent: UserAgent let acceptEncoding: String = "gzip;q=1.0, compress;q=0.5" let acceptLanguage: String = { @@ -48,7 +47,7 @@ public extension APIRequest { self.etag = etag self.additionalHeaders = additionalHeaders } - + public var httpHeaders: HTTPHeaders { var headers = [ HTTPHeaderField.acceptEncoding: acceptEncoding, @@ -63,7 +62,7 @@ public extension APIRequest { } return headers } - + } - + } diff --git a/Sources/Networking/APIRequest.swift b/Sources/Networking/APIRequest.swift index 6d374f9d4..40409e1e7 100644 --- a/Sources/Networking/APIRequest.swift +++ b/Sources/Networking/APIRequest.swift @@ -1,6 +1,5 @@ // // APIRequest.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -24,7 +23,7 @@ public typealias APIResponse = (data: Data?, response: HTTPURLResponse) public typealias APIRequestCompletion = (APIResponse?, APIRequest.Error?) -> Void public struct APIRequest { - + private let request: URLRequest private let requirements: APIResponseRequirements private let urlSession: URLSession @@ -41,10 +40,10 @@ public struct APIRequest { self.requirements = requirements self.urlSession = urlSession self.getLog = log - + assertUserAgentIsPresent() } - + private func assertUserAgentIsPresent() { guard request.allHTTPHeaderFields?[HTTPHeaderField.userAgent] != nil else { assertionFailure("A user agent must be included in the request's HTTP header fields.") @@ -77,7 +76,7 @@ public struct APIRequest { task.resume() return task } - + private func validateAndUnwrap(data: Data?, response: URLResponse) throws -> APIResponse { let httpResponse = try response.asHTTPURLResponse() @@ -87,7 +86,7 @@ public struct APIRequest { request.httpMethod ?? "", request.url?.absoluteString ?? "", httpResponse.statusCode) - + var data = data if requirements.contains(.allowHTTPNotModified), httpResponse.statusCode == HTTPURLResponse.Constants.notModifiedStatusCode { data = nil // avoid returning empty data @@ -98,11 +97,11 @@ public struct APIRequest { throw APIRequest.Error.emptyData } } - + if requirements.contains(.requireETagHeader), httpResponse.etag == nil { throw APIRequest.Error.missingEtagInResponse } - + return (data, httpResponse) } @@ -116,7 +115,7 @@ public struct APIRequest { let (data, response) = try await fetch(for: request) return try validateAndUnwrap(data: data, response: response) } - + private func fetch(for request: URLRequest) async throws -> (Data, URLResponse) { do { return try await urlSession.data(for: request) diff --git a/Sources/Networking/APIRequestConfiguration.swift b/Sources/Networking/APIRequestConfiguration.swift index a9af67a0d..b9a7b9387 100644 --- a/Sources/Networking/APIRequestConfiguration.swift +++ b/Sources/Networking/APIRequestConfiguration.swift @@ -1,6 +1,5 @@ // // APIRequestConfiguration.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,9 +20,9 @@ import Foundation import Common extension APIRequest { - + public struct Configuration where QueryParams.Element == (key: String, value: String) { - + let url: URL let method: HTTPMethod let queryParameters: QueryParams @@ -33,7 +32,7 @@ extension APIRequest { let timeoutInterval: TimeInterval let attribution: URLRequestAttribution? let cachePolicy: URLRequest.CachePolicy? - + public init(url: URL, method: HTTPMethod = .get, queryParameters: QueryParams = [], @@ -53,7 +52,7 @@ extension APIRequest { self.attribution = attribution self.cachePolicy = cachePolicy } - + var request: URLRequest { let url = url.appendingParameters(queryParameters, allowedReservedCharacters: allowedQueryReservedCharacters) var request = URLRequest(url: url, timeoutInterval: timeoutInterval) @@ -70,7 +69,7 @@ extension APIRequest { } return request } - + } - + } diff --git a/Sources/Networking/APIRequestError.swift b/Sources/Networking/APIRequestError.swift index b85def5d3..98e54df6f 100644 --- a/Sources/Networking/APIRequestError.swift +++ b/Sources/Networking/APIRequestError.swift @@ -1,6 +1,5 @@ // // APIRequestError.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,15 +19,15 @@ import Foundation extension APIRequest { - + public enum Error: Swift.Error, LocalizedError { - + case urlSession(Swift.Error) case invalidResponse case missingEtagInResponse case emptyData case invalidStatusCode(Int) - + public var errorDescription: String? { switch self { case .urlSession(let error): @@ -44,5 +43,5 @@ extension APIRequest { } } } - + } diff --git a/Sources/Networking/APIResponseRequirements.swift b/Sources/Networking/APIResponseRequirements.swift index a97498bfe..32c0ea35b 100644 --- a/Sources/Networking/APIResponseRequirements.swift +++ b/Sources/Networking/APIResponseRequirements.swift @@ -1,6 +1,5 @@ // -// APIResponseRequirement.swift -// DuckDuckGo +// APIResponseRequirements.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,12 +19,12 @@ import Foundation public struct APIResponseRequirements: OptionSet { - + public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } - + /// The API response must have non-empty data. public static let requireNonEmptyData = APIResponseRequirements(rawValue: 1 << 0) /// The API response must include an ETag header. @@ -33,8 +32,8 @@ public struct APIResponseRequirements: OptionSet { /// Allows HTTP 304 (Not Modified) response status code. /// When this is set, requireNonEmptyData is not honored, since URLSession returns empty data on HTTP 304. public static let allowHTTPNotModified = APIResponseRequirements(rawValue: 1 << 2) - + public static let `default`: APIResponseRequirements = [.requireNonEmptyData, .requireETagHeader] public static let all: APIResponseRequirements = [.requireNonEmptyData, .requireETagHeader, .allowHTTPNotModified] - + } diff --git a/Sources/Networking/Extensions/HTTPConstants.swift b/Sources/Networking/Extensions/HTTPConstants.swift index f83198279..e1ceff9f5 100644 --- a/Sources/Networking/Extensions/HTTPConstants.swift +++ b/Sources/Networking/Extensions/HTTPConstants.swift @@ -1,6 +1,5 @@ // // HTTPConstants.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,20 +19,20 @@ import Foundation extension APIRequest { - + public enum HTTPHeaderField { - + public static let acceptEncoding = "Accept-Encoding" public static let acceptLanguage = "Accept-Language" public static let userAgent = "User-Agent" public static let etag = "ETag" public static let ifNoneMatch = "If-None-Match" public static let moreInfo = "X-DuckDuckGo-MoreInfo" - + } - + public enum HTTPMethod: String { - + case get = "GET" case head = "HEAD" case post = "POST" @@ -43,7 +42,7 @@ extension APIRequest { case options = "OPTIONS" case trace = "TRACE" case patch = "PATCH" - + } - + } diff --git a/Sources/Networking/Extensions/HTTPURLResponseExtension.swift b/Sources/Networking/Extensions/HTTPURLResponseExtension.swift index 498975c37..26c9a9dcd 100644 --- a/Sources/Networking/Extensions/HTTPURLResponseExtension.swift +++ b/Sources/Networking/Extensions/HTTPURLResponseExtension.swift @@ -1,6 +1,5 @@ // // HTTPURLResponseExtension.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,23 +20,23 @@ import Foundation import Common public extension HTTPURLResponse { - + enum Constants { - + static let weakEtagPrefix = "W/" static let successfulStatusCodes = 200..<300 static let notModifiedStatusCode = 304 - + } - + func assertStatusCode(_ acceptedStatusCodes: S) throws where S.Iterator.Element == Int { guard acceptedStatusCodes.contains(statusCode) else { throw APIRequest.Error.invalidStatusCode(statusCode) } } - + func assertSuccessfulStatusCode() throws { try assertStatusCode(Constants.successfulStatusCodes) } - + var isSuccessfulResponse: Bool { do { try assertSuccessfulStatusCode() @@ -46,7 +45,7 @@ public extension HTTPURLResponse { return false } } - + func etag(droppingWeakPrefix: Bool) -> String? { let etag = value(forHTTPHeaderField: APIRequest.HTTPHeaderField.etag) if droppingWeakPrefix { @@ -54,7 +53,7 @@ public extension HTTPURLResponse { } return etag } - + var etag: String? { etag(droppingWeakPrefix: true) } - + } diff --git a/Sources/Networking/Extensions/URLRequestAttribution.swift b/Sources/Networking/Extensions/URLRequestAttribution.swift index 27a3e9221..d54e88cdb 100644 --- a/Sources/Networking/Extensions/URLRequestAttribution.swift +++ b/Sources/Networking/Extensions/URLRequestAttribution.swift @@ -1,6 +1,5 @@ // // URLRequestAttribution.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,11 +20,11 @@ import Foundation import Common public enum URLRequestAttribution { - + case unattributed case developer case user - + @available(iOS 15.0, macOS 12.0, *) public var urlRequestAttribution: URLRequest.Attribution? { switch self { @@ -37,5 +36,5 @@ public enum URLRequestAttribution { return nil } } - + } diff --git a/Sources/Networking/Extensions/URLResponseExtension.swift b/Sources/Networking/Extensions/URLResponseExtension.swift index f00a75fe0..e930bee90 100644 --- a/Sources/Networking/Extensions/URLResponseExtension.swift +++ b/Sources/Networking/Extensions/URLResponseExtension.swift @@ -1,6 +1,5 @@ // // URLResponseExtension.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,12 +19,12 @@ import Foundation extension URLResponse { - + func asHTTPURLResponse() throws -> HTTPURLResponse { guard let httpResponse = self as? HTTPURLResponse else { throw APIRequest.Error.invalidResponse } return httpResponse } - + } diff --git a/Sources/Networking/Extensions/URLSessionExtension.swift b/Sources/Networking/Extensions/URLSessionExtension.swift index 39e82c562..367bee1d5 100644 --- a/Sources/Networking/Extensions/URLSessionExtension.swift +++ b/Sources/Networking/Extensions/URLSessionExtension.swift @@ -1,6 +1,5 @@ // // URLSessionExtension.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -20,7 +19,7 @@ import Foundation extension URLSession { - + private static var defaultCallbackQueue: OperationQueue = { let queue = OperationQueue() queue.name = "APIRequest default callback queue" @@ -28,7 +27,7 @@ extension URLSession { queue.maxConcurrentOperationCount = 1 return queue }() - + private static let defaultCallback = URLSession(configuration: .default, delegate: nil, delegateQueue: defaultCallbackQueue) private static let defaultCallbackEphemeral = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: defaultCallbackQueue) @@ -42,5 +41,5 @@ extension URLSession { return ephemeral ? defaultCallbackEphemeral : defaultCallback } } - + } diff --git a/Sources/Persistence/CoreDataDatabase.swift b/Sources/Persistence/CoreDataDatabase.swift index 63b41224e..f29ba2d5d 100644 --- a/Sources/Persistence/CoreDataDatabase.swift +++ b/Sources/Persistence/CoreDataDatabase.swift @@ -21,12 +21,12 @@ import CoreData import Common public protocol ManagedObjectContextFactory { - + func makeContext(concurrencyType: NSManagedObjectContextConcurrencyType, name: String?) -> NSManagedObjectContext } public class CoreDataDatabase: ManagedObjectContextFactory { - + public enum Error: Swift.Error { case containerLocationCouldNotBePrepared(underlyingError: Swift.Error) } @@ -40,15 +40,15 @@ public class CoreDataDatabase: ManagedObjectContextFactory { return FileManager.default.fileExists(atPath: containerURL.path) } - + public var model: NSManagedObjectModel { return container.managedObjectModel } - + public var coordinator: NSPersistentStoreCoordinator { return container.persistentStoreCoordinator } - + public static func loadModel(from bundle: Bundle, named name: String) -> NSManagedObjectModel? { let momdUrl = bundle.url(forResource: name, withExtension: "momd") ?? bundle.resourceURL!.appendingPathComponent(name + ".momd") @@ -69,19 +69,19 @@ public class CoreDataDatabase: ManagedObjectContextFactory { } #endif guard FileManager.default.fileExists(atPath: momdUrl.path) else { return nil } - + return NSManagedObjectModel(contentsOf: momdUrl) } - + public init(name: String, containerLocation: URL, model: NSManagedObjectModel, readOnly: Bool = false, options: [String: NSObject] = [:]) { - + self.container = NSPersistentContainer(name: name, managedObjectModel: model) self.containerLocation = containerLocation - + let description = NSPersistentStoreDescription(url: containerLocation.appendingPathComponent("\(name).sqlite")) description.type = NSSQLiteStoreType description.isReadOnly = readOnly @@ -89,25 +89,25 @@ public class CoreDataDatabase: ManagedObjectContextFactory { for (key, value) in options { description.setOption(value, forKey: key) } - + self.container.persistentStoreDescriptions = [description] } - + public func loadStore(completion: @escaping (NSManagedObjectContext?, Swift.Error?) -> Void = { _, _ in }) { - + do { try FileManager.default.createDirectory(at: containerLocation, withIntermediateDirectories: true) } catch { completion(nil, Error.containerLocationCouldNotBePrepared(underlyingError: error)) return } - + container.loadPersistentStores { _, error in if let error = error { completion(nil, error) return } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.persistentStoreCoordinator = self.container.persistentStoreCoordinator context.name = "Migration" @@ -117,7 +117,7 @@ public class CoreDataDatabase: ManagedObjectContextFactory { } } } - + public func tearDown(deleteStores: Bool) throws { typealias StoreInfo = (url: URL?, type: String) var storesToDelete = [StoreInfo]() @@ -125,7 +125,7 @@ public class CoreDataDatabase: ManagedObjectContextFactory { storesToDelete.append((url: store.url, type: store.type)) try container.persistentStoreCoordinator.remove(store) } - + if deleteStores { for (url, type) in storesToDelete { if let url = url { @@ -134,14 +134,14 @@ public class CoreDataDatabase: ManagedObjectContextFactory { } } } - + public func makeContext(concurrencyType: NSManagedObjectContextConcurrencyType, name: String? = nil) -> NSManagedObjectContext { RunLoop.current.run(until: storeLoadedCondition) let context = NSManagedObjectContext(concurrencyType: concurrencyType) context.persistentStoreCoordinator = container.persistentStoreCoordinator context.name = name - + return context } } @@ -164,7 +164,7 @@ extension NSManagedObjectContext { for entityDescription in entityDescriptions { let request = NSFetchRequest() request.entity = entityDescription - + deleteAll(matching: request) } } diff --git a/Sources/Persistence/CoreDataErrorsParser.swift b/Sources/Persistence/CoreDataErrorsParser.swift index a8c9b450e..b915054c5 100644 --- a/Sources/Persistence/CoreDataErrorsParser.swift +++ b/Sources/Persistence/CoreDataErrorsParser.swift @@ -1,6 +1,6 @@ // // CoreDataErrorsParser.swift -// +// // Copyright © 2022 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,39 +20,39 @@ import Foundation import CoreData public class CoreDataErrorsParser { - + public struct ErrorInfo: Equatable { public let code: Int public let domain: String public let entity: String? public let property: String? } - + public static func parse(error: NSError) -> [ErrorInfo] { - + let unwrapped = unwrapErrorIfNeeded(error) return unwrapped.compactMap(checkError(_:)) } - + private static func unwrapErrorIfNeeded(_ error: NSError) -> [NSError] { if let errors = error.userInfo[NSDetailedErrorsKey] as? [NSError] { return errors } return [error] } - + private static func checkError(_ error: NSError) -> ErrorInfo { if let info = checkValidationError(error) { return info } - + if let info = checkConflictError(error) { return info } - + return ErrorInfo(code: error.code, domain: error.domain, entity: nil, property: nil) } - + private static func checkValidationError(_ error: NSError) -> ErrorInfo? { guard let validationInfo = error.userInfo[NSValidationKeyErrorKey] as? String, let managedObject = error.userInfo[NSValidationObjectErrorKey] as? NSManagedObject else { @@ -63,15 +63,15 @@ public class CoreDataErrorsParser { entity: managedObject.entity.name, property: validationInfo) } - + private static func checkConflictError(_ error: NSError) -> ErrorInfo? { guard error.code == NSManagedObjectMergeError, let conflicts = error.userInfo[NSPersistentStoreSaveConflictsErrorKey] as? [NSMergeConflict], let firstConflict = conflicts.first else { return nil } - + return ErrorInfo(code: error.code, domain: error.domain, entity: firstConflict.sourceObject.entity.name, property: nil) } - + } diff --git a/Sources/Persistence/KeyValueStoring.swift b/Sources/Persistence/KeyValueStoring.swift index c82b56b42..f1e912423 100644 --- a/Sources/Persistence/KeyValueStoring.swift +++ b/Sources/Persistence/KeyValueStoring.swift @@ -1,6 +1,5 @@ // // KeyValueStoring.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -21,7 +20,7 @@ import Foundation /// Key-value store compatible with base UserDefaults API public protocol KeyValueStoring { - + func object(forKey defaultName: String) -> Any? func set(_ value: Any?, forKey defaultName: String) func removeObject(forKey defaultName: String) diff --git a/Sources/PrivacyDashboard/Model/AllowedPermission.swift b/Sources/PrivacyDashboard/Model/AllowedPermission.swift index 1d73c60e3..c4d36e9ea 100644 --- a/Sources/PrivacyDashboard/Model/AllowedPermission.swift +++ b/Sources/PrivacyDashboard/Model/AllowedPermission.swift @@ -26,7 +26,7 @@ public struct AllowedPermission: Codable { var used: Bool var paused: Bool var options: [[String: String]] - + public init(key: String, icon: String, title: String, diff --git a/Sources/PrivacyDashboard/Model/CookieConsentInfo.swift b/Sources/PrivacyDashboard/Model/CookieConsentInfo.swift index 475d24931..c86872932 100644 --- a/Sources/PrivacyDashboard/Model/CookieConsentInfo.swift +++ b/Sources/PrivacyDashboard/Model/CookieConsentInfo.swift @@ -24,7 +24,7 @@ public struct CookieConsentInfo: Encodable { let optoutFailed: Bool? let selftestFailed: Bool? let configurable = true - + public init(consentManaged: Bool, cosmetic: Bool?, optoutFailed: Bool?, selftestFailed: Bool?) { self.consentManaged = consentManaged self.cosmetic = cosmetic diff --git a/Sources/PrivacyDashboard/Model/ProtectionStatus.swift b/Sources/PrivacyDashboard/Model/ProtectionStatus.swift index 2fc310faa..af69ba1a5 100644 --- a/Sources/PrivacyDashboard/Model/ProtectionStatus.swift +++ b/Sources/PrivacyDashboard/Model/ProtectionStatus.swift @@ -1,6 +1,5 @@ // // ProtectionStatus.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -20,12 +19,12 @@ import Foundation public struct ProtectionStatus: Encodable { - + let unprotectedTemporary: Bool let enabledFeatures: [String] let allowlisted: Bool let denylisted: Bool - + public init(unprotectedTemporary: Bool, enabledFeatures: [String], allowlisted: Bool, denylisted: Bool) { self.unprotectedTemporary = unprotectedTemporary self.enabledFeatures = enabledFeatures diff --git a/Sources/PrivacyDashboard/Model/TrackerInfo.swift b/Sources/PrivacyDashboard/Model/TrackerInfo.swift index 8b5b051b7..5494c40da 100644 --- a/Sources/PrivacyDashboard/Model/TrackerInfo.swift +++ b/Sources/PrivacyDashboard/Model/TrackerInfo.swift @@ -21,51 +21,51 @@ import TrackerRadarKit import ContentBlocking public struct TrackerInfo: Encodable { - + enum CodingKeys: String, CodingKey { case requests case installedSurrogates } - + public private (set) var trackers = Set() private(set) var thirdPartyRequests = Set() public private(set) var installedSurrogates = Set() - + public init() { } - + // MARK: - Collecting detected elements - + public mutating func addDetectedTracker(_ tracker: DetectedRequest, onPageWithURL url: URL) { guard tracker.pageUrl == url.absoluteString else { return } trackers.insert(tracker) } - + public mutating func add(detectedThirdPartyRequest request: DetectedRequest) { thirdPartyRequests.insert(request) } - + public mutating func addInstalledSurrogateHost(_ host: String, for tracker: DetectedRequest, onPageWithURL url: URL) { guard tracker.pageUrl == url.absoluteString else { return } installedSurrogates.insert(host) } - + // MARK: - Helper accessors - + public var trackersBlocked: [DetectedRequest] { trackers.filter { $0.state == .blocked } } - + public var trackersDetected: [DetectedRequest] { trackers.filter { $0.state != .blocked } } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - + let allRequests = [] + trackers + thirdPartyRequests - + try container.encode(allRequests, forKey: .requests) try container.encode(installedSurrogates, forKey: .installedSurrogates) } - + } diff --git a/Sources/PrivacyDashboard/PrivacyDashboardController.swift b/Sources/PrivacyDashboard/PrivacyDashboardController.swift index 9c715d5ef..28996dd60 100644 --- a/Sources/PrivacyDashboard/PrivacyDashboardController.swift +++ b/Sources/PrivacyDashboard/PrivacyDashboardController.swift @@ -31,34 +31,34 @@ public protocol PrivacyDashboardNavigationDelegate: AnyObject { #if os(iOS) func privacyDashboardControllerDidTapClose(_ privacyDashboardController: PrivacyDashboardController) #endif - + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didSetHeight height: Int) } /// `Report broken site` web page delegate public protocol PrivacyDashboardReportBrokenSiteDelegate: AnyObject { - - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestSubmitBrokenSiteReportWithCategory category: String, description: String) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, reportBrokenSiteDidChangeProtectionSwitch protectionState: ProtectionState) } /// `Privacy Dasboard` web page delegate public protocol PrivacyDashboardControllerDelegate: AnyObject { - - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didChangeProtectionSwitch protectionState: ProtectionState) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestOpenUrlInNewTab url: URL) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didRequestOpenSettings target: PrivacyDashboardOpenSettingsTarget) func privacyDashboardControllerDidRequestShowReportBrokenSite(_ privacyDashboardController: PrivacyDashboardController) - + #if os(macOS) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, didSetPermission permissionName: String, to state: PermissionAuthorizationState) - func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, + func privacyDashboardController(_ privacyDashboardController: PrivacyDashboardController, setPermission permissionName: String, paused: Bool) #endif } @@ -68,25 +68,25 @@ public protocol PrivacyDashboardControllerDelegate: AnyObject { /// 2- Direct access to the `Report broken site` page /// Which flow is used is decided at `setup(...)` time, where if `reportBrokenSiteOnly` is true then the `Report broken site` page is opened directly. @MainActor public final class PrivacyDashboardController: NSObject { - + // Delegates public weak var privacyDashboardDelegate: PrivacyDashboardControllerDelegate? public weak var privacyDashboardNavigationDelegate: PrivacyDashboardNavigationDelegate? public weak var privacyDashboardReportBrokenSiteDelegate: PrivacyDashboardReportBrokenSiteDelegate? - + @Published public var theme: PrivacyDashboardTheme? public var preferredLocale: String? @Published public var allowedPermissions: [AllowedPermission] = [] public private(set) weak var privacyInfo: PrivacyInfo? - + private weak var webView: WKWebView? private let privacyDashboardScript = PrivacyDashboardUserScript() private var cancellables = Set() - + public init(privacyInfo: PrivacyInfo?) { self.privacyInfo = privacyInfo } - + /// Configure the webview for showing `Privacy Dasboard` or `Report broken site` /// - Parameters: /// - webView: The webview to use @@ -94,73 +94,73 @@ public protocol PrivacyDashboardControllerDelegate: AnyObject { public func setup(for webView: WKWebView, reportBrokenSiteOnly: Bool) { self.webView = webView webView.navigationDelegate = self - + setupPrivacyDashboardUserScript() loadPrivacyDashboardHTML(reportBrokenSiteOnly: reportBrokenSiteOnly) } - + public func updatePrivacyInfo(_ privacyInfo: PrivacyInfo?) { cancellables.removeAll() self.privacyInfo = privacyInfo - + subscribeToDataModelChanges() sendProtectionStatus() } - + public func cleanUp() { cancellables.removeAll() - + privacyDashboardScript.messageNames.forEach { messageName in webView?.configuration.userContentController.removeScriptMessageHandler(forName: messageName) } } - + public func didStartRulesCompilation() { guard let webView = self.webView else { return } privacyDashboardScript.setIsPendingUpdates(true, webView: webView) } - + public func didFinishRulesCompilation() { guard let webView = self.webView else { return } privacyDashboardScript.setIsPendingUpdates(false, webView: webView) } - + private func setupPrivacyDashboardUserScript() { guard let webView = self.webView else { return } - + privacyDashboardScript.delegate = self - + webView.configuration.userContentController.addUserScript(privacyDashboardScript.makeWKUserScriptSync()) - + privacyDashboardScript.messageNames.forEach { messageName in webView.configuration.userContentController.add(privacyDashboardScript, name: messageName) } } - + private func loadPrivacyDashboardHTML(reportBrokenSiteOnly: Bool) { guard var url = Bundle.privacyDashboardURL else { return } - + if reportBrokenSiteOnly { url = url.appendingParameter(name: "screen", value: ProtectionState.EventOriginScreen.breakageForm.rawValue) } - + webView?.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent().deletingLastPathComponent()) } } extension PrivacyDashboardController: WKNavigationDelegate { - + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { subscribeToDataModelChanges() - + sendProtectionStatus() sendParentEntity() sendCurrentLocale() } - + private func subscribeToDataModelChanges() { cancellables.removeAll() - + subscribeToTheme() subscribeToTrackerInfo() subscribeToConnectionUpgradedTo() @@ -168,7 +168,7 @@ extension PrivacyDashboardController: WKNavigationDelegate { subscribeToConsentManaged() subscribeToAllowedPermissions() } - + private func subscribeToTheme() { $theme .removeDuplicates() @@ -179,7 +179,7 @@ extension PrivacyDashboardController: WKNavigationDelegate { }) .store(in: &cancellables) } - + private func subscribeToTrackerInfo() { privacyInfo?.$trackerInfo .receive(on: DispatchQueue.main) @@ -190,7 +190,7 @@ extension PrivacyDashboardController: WKNavigationDelegate { }) .store(in: &cancellables) } - + private func subscribeToConnectionUpgradedTo() { privacyInfo?.$connectionUpgradedTo .receive(on: DispatchQueue.main) @@ -201,7 +201,7 @@ extension PrivacyDashboardController: WKNavigationDelegate { }) .store(in: &cancellables) } - + private func subscribeToServerTrust() { privacyInfo?.$serverTrust .receive(on: DispatchQueue.global(qos: .userInitiated)) @@ -215,7 +215,7 @@ extension PrivacyDashboardController: WKNavigationDelegate { }) .store(in: &cancellables) } - + private func subscribeToConsentManaged() { privacyInfo?.$cookieConsentManaged .receive(on: DispatchQueue.main) @@ -225,7 +225,7 @@ extension PrivacyDashboardController: WKNavigationDelegate { }) .store(in: &cancellables) } - + private func subscribeToAllowedPermissions() { $allowedPermissions .receive(on: DispatchQueue.main) @@ -235,75 +235,75 @@ extension PrivacyDashboardController: WKNavigationDelegate { }) .store(in: &cancellables) } - + private func sendProtectionStatus() { guard let webView = self.webView, let protectionStatus = privacyInfo?.protectionStatus else { return } - + privacyDashboardScript.setProtectionStatus(protectionStatus, webView: webView) } - + private func sendParentEntity() { guard let webView = self.webView else { return } privacyDashboardScript.setParentEntity(privacyInfo?.parentEntity, webView: webView) } - + private func sendCurrentLocale() { guard let webView = self.webView else { return } - + let locale = preferredLocale ?? "en" privacyDashboardScript.setLocale(locale, webView: webView) } } extension PrivacyDashboardController: PrivacyDashboardUserScriptDelegate { - + func userScript(_ userScript: PrivacyDashboardUserScript, didRequestOpenSettings target: String) { let settingsTarget = PrivacyDashboardOpenSettingsTarget(rawValue: target) ?? .general privacyDashboardDelegate?.privacyDashboardController(self, didRequestOpenSettings: settingsTarget) } - + func userScript(_ userScript: PrivacyDashboardUserScript, didChangeProtectionState protectionState: ProtectionState) { - + switch protectionState.eventOrigin.screen { case .primaryScreen: privacyDashboardDelegate?.privacyDashboardController(self, didChangeProtectionSwitch: protectionState) case .breakageForm: privacyDashboardReportBrokenSiteDelegate?.privacyDashboardController(self, reportBrokenSiteDidChangeProtectionSwitch: protectionState) } - + } - + func userScript(_ userScript: PrivacyDashboardUserScript, didRequestOpenUrlInNewTab url: URL) { privacyDashboardDelegate?.privacyDashboardController(self, didRequestOpenUrlInNewTab: url) } - + func userScriptDidRequestClosing(_ userScript: PrivacyDashboardUserScript) { #if os(iOS) privacyDashboardNavigationDelegate?.privacyDashboardControllerDidTapClose(self) #endif } - + func userScriptDidRequestShowReportBrokenSite(_ userScript: PrivacyDashboardUserScript) { privacyDashboardDelegate?.privacyDashboardControllerDidRequestShowReportBrokenSite(self) } - + func userScript(_ userScript: PrivacyDashboardUserScript, setHeight height: Int) { privacyDashboardNavigationDelegate?.privacyDashboardController(self, didSetHeight: height) } - + func userScript(_ userScript: PrivacyDashboardUserScript, didRequestSubmitBrokenSiteReportWithCategory category: String, description: String) { - privacyDashboardReportBrokenSiteDelegate?.privacyDashboardController(self, didRequestSubmitBrokenSiteReportWithCategory: category, + privacyDashboardReportBrokenSiteDelegate?.privacyDashboardController(self, didRequestSubmitBrokenSiteReportWithCategory: category, description: description) } - + func userScript(_ userScript: PrivacyDashboardUserScript, didSetPermission permission: String, to state: PermissionAuthorizationState) { #if os(macOS) privacyDashboardDelegate?.privacyDashboardController(self, didSetPermission: permission, to: state) #endif } - + func userScript(_ userScript: PrivacyDashboardUserScript, setPermission permission: String, paused: Bool) { #if os(macOS) privacyDashboardDelegate?.privacyDashboardController(self, setPermission: permission, paused: paused) diff --git a/Sources/PrivacyDashboard/PrivacyInfo.swift b/Sources/PrivacyDashboard/PrivacyInfo.swift index fd488e382..a4ca9265f 100644 --- a/Sources/PrivacyDashboard/PrivacyInfo.swift +++ b/Sources/PrivacyDashboard/PrivacyInfo.swift @@ -21,32 +21,32 @@ import TrackerRadarKit import Common public final class PrivacyInfo { - + public private(set) var url: URL private(set) var parentEntity: Entity? - + @Published public var trackerInfo: TrackerInfo @Published private(set) var protectionStatus: ProtectionStatus @Published public var serverTrust: SecTrust? @Published public var connectionUpgradedTo: URL? @Published public var cookieConsentManaged: CookieConsentInfo? - + public init(url: URL, parentEntity: Entity?, protectionStatus: ProtectionStatus) { self.url = url self.parentEntity = parentEntity self.protectionStatus = protectionStatus - + trackerInfo = TrackerInfo() } - + public var https: Bool { return url.isHttps } - + public var domain: String? { return url.host } - + public func isFor(_ url: URL?) -> Bool { return self.url.host == url?.host } diff --git a/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift b/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift index 1df481f4d..5c57ef390 100644 --- a/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift +++ b/Sources/PrivacyDashboard/UserScript/PrivacyDashboardUserScript.swift @@ -42,11 +42,11 @@ public enum PrivacyDashboardTheme: String, Encodable { public struct ProtectionState: Decodable { public let isProtected: Bool public let eventOrigin: EventOrigin - + public struct EventOrigin: Decodable { public let screen: EventOriginScreen } - + public enum EventOriginScreen: String, Decodable { case primaryScreen case breakageForm @@ -54,7 +54,7 @@ public struct ProtectionState: Decodable { } final class PrivacyDashboardUserScript: NSObject, StaticUserScript { - + enum MessageNames: String, CaseIterable { case privacyDashboardSetProtection case privacyDashboardSetSize @@ -66,21 +66,21 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { case privacyDashboardSetPermission case privacyDashboardSetPermissionPaused } - + static var injectionTime: WKUserScriptInjectionTime { .atDocumentStart } static var forMainFrameOnly: Bool { false } static var source: String = "" static var script: WKUserScript = PrivacyDashboardUserScript.makeWKUserScript() var messageNames: [String] { MessageNames.allCases.map(\.rawValue) } - + weak var delegate: PrivacyDashboardUserScriptDelegate? - + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let messageType = MessageNames(rawValue: message.name) else { assertionFailure("PrivacyDashboardUserScript: unexpected message name \(message.name)") return } - + switch messageType { case .privacyDashboardSetProtection: handleSetProtection(message: message) @@ -102,37 +102,37 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { handleOpenSettings(message: message) } } - + // MARK: - JS message handlers - + private func handleSetProtection(message: WKScriptMessage) { - + guard let protectionState: ProtectionState = DecodableHelper.decode(from: message.messageBody) else { assertionFailure("privacyDashboardSetProtection: expected ProtectionState") return } - + delegate?.userScript(self, didChangeProtectionState: protectionState) } - + private func handleSetSize(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let height = dict["height"] as? Int else { assertionFailure("privacyDashboardSetHeight: expected height to be an Int") return } - + delegate?.userScript(self, setHeight: height) } - + private func handleClose() { delegate?.userScriptDidRequestClosing(self) } - + private func handleShowReportBrokenSite() { delegate?.userScriptDidRequestShowReportBrokenSite(self) } - + private func handleSubmitBrokenSiteReport(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let category = dict["category"] as? String, @@ -140,10 +140,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("privacyDashboardSetHeight: expected { category: String, description: String }") return } - + delegate?.userScript(self, didRequestSubmitBrokenSiteReportWithCategory: category, description: description) } - + private func handleOpenUrlInNewTab(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let urlString = dict["url"] as? String, @@ -152,10 +152,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("handleOpenUrlInNewTab: expected { url: '...' } ") return } - + delegate?.userScript(self, didRequestOpenUrlInNewTab: url) } - + private func handleOpenSettings(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let target = dict["target"] as? String @@ -163,10 +163,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("handleOpenSettings: expected { target: '...' } ") return } - + delegate?.userScript(self, didRequestOpenSettings: target) } - + private func handleSetPermission(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let permission = dict["permission"] as? String, @@ -175,10 +175,10 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("privacyDashboardSetPermission: expected { permission: PermissionType, value: PermissionAuthorizationState }") return } - + delegate?.userScript(self, didSetPermission: permission, to: state) } - + private func handleSetPermissionPaused(message: WKScriptMessage) { guard let dict = message.body as? [String: Any], let permission = dict["permission"] as? String, @@ -187,86 +187,86 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { assertionFailure("handleSetPermissionPaused: expected { permission: PermissionType, paused: Bool }") return } - + delegate?.userScript(self, setPermission: permission, paused: paused) } - + // MARK: - Calls to script's JS API - + func setTrackerInfo(_ tabUrl: URL, trackerInfo: TrackerInfo, webView: WKWebView) { guard let trackerBlockingDataJson = try? JSONEncoder().encode(trackerInfo).utf8String() else { assertionFailure("Can't encode trackerInfoViewModel into JSON") return } - + guard let safeTabUrl = try? JSONEncoder().encode(tabUrl).utf8String() else { assertionFailure("Can't encode tabUrl into JSON") return } - + evaluate(js: "window.onChangeRequestData(\(safeTabUrl), \(trackerBlockingDataJson))", in: webView) } - + func setProtectionStatus(_ protectionStatus: ProtectionStatus, webView: WKWebView) { guard let protectionStatusJson = try? JSONEncoder().encode(protectionStatus).utf8String() else { assertionFailure("Can't encode mockProtectionStatus into JSON") return } - + evaluate(js: "window.onChangeProtectionStatus(\(protectionStatusJson))", in: webView) } - + func setUpgradedHttps(_ upgradedHttps: Bool, webView: WKWebView) { evaluate(js: "window.onChangeUpgradedHttps(\(upgradedHttps))", in: webView) } - + func setParentEntity(_ parentEntity: Entity?, webView: WKWebView) { if parentEntity == nil { return } - + guard let parentEntityJson = try? JSONEncoder().encode(parentEntity).utf8String() else { assertionFailure("Can't encode parentEntity into JSON") return } - + evaluate(js: "window.onChangeParentEntity(\(parentEntityJson))", in: webView) } - + func setTheme(_ theme: PrivacyDashboardTheme?, webView: WKWebView) { if theme == nil { return } - + guard let themeJson = try? JSONEncoder().encode(theme).utf8String() else { assertionFailure("Can't encode themeName into JSON") return } - + evaluate(js: "window.onChangeTheme(\(themeJson))", in: webView) } - + func setServerTrust(_ serverTrustViewModel: ServerTrustViewModel, webView: WKWebView) { guard let certificateDataJson = try? JSONEncoder().encode(serverTrustViewModel).utf8String() else { assertionFailure("Can't encode serverTrustViewModel into JSON") return } - + evaluate(js: "window.onChangeCertificateData(\(certificateDataJson))", in: webView) } - + func setIsPendingUpdates(_ isPendingUpdates: Bool, webView: WKWebView) { evaluate(js: "window.onIsPendingUpdates(\(isPendingUpdates))", in: webView) } - + func setLocale(_ currentLocale: String, webView: WKWebView) { struct LocaleSetting: Encodable { var locale: String } - + guard let localeSettingJson = try? JSONEncoder().encode(LocaleSetting(locale: currentLocale)).utf8String() else { assertionFailure("Can't encode consentInfo into JSON") return } evaluate(js: "window.onChangeLocale(\(localeSettingJson))", in: webView) } - + func setConsentManaged(_ consentManaged: CookieConsentInfo?, webView: WKWebView) { guard let consentDataJson = try? JSONEncoder().encode(consentManaged).utf8String() else { assertionFailure("Can't encode consentInfo into JSON") @@ -274,26 +274,26 @@ final class PrivacyDashboardUserScript: NSObject, StaticUserScript { } evaluate(js: "window.onChangeConsentManaged(\(consentDataJson))", in: webView) } - + func setPermissions(allowedPermissions: [AllowedPermission], webView: WKWebView) { guard let allowedPermissionsJson = try? JSONEncoder().encode(allowedPermissions).utf8String() else { assertionFailure("PrivacyDashboardUserScript: could not serialize permissions object") return } - + self.evaluate(js: "window.onChangeAllowedPermissions(\(allowedPermissionsJson))", in: webView) } - + private func evaluate(js: String, in webView: WKWebView) { webView.evaluateJavaScript(js) } - + } extension Data { - + func utf8String() -> String? { return String(data: self, encoding: .utf8) } - + } diff --git a/Sources/PrivacyDashboard/ViewModel/ServerTrustViewModel.swift b/Sources/PrivacyDashboard/ViewModel/ServerTrustViewModel.swift index 0862cb89d..eb94d1db3 100644 --- a/Sources/PrivacyDashboard/ViewModel/ServerTrustViewModel.swift +++ b/Sources/PrivacyDashboard/ViewModel/ServerTrustViewModel.swift @@ -19,28 +19,28 @@ import Foundation public struct ServerTrustViewModel: Encodable { - + struct SecCertificateViewModel: Encodable { - + let summary: String? let commonName: String? let emails: [String]? let publicKey: SecKeyViewModel? - + public init(secCertificate: SecCertificate) { summary = SecCertificateCopySubjectSummary(secCertificate) as String? ?? "" - + var commonName: CFString? SecCertificateCopyCommonName(secCertificate, &commonName) self.commonName = commonName as String? ?? "" - + var emails: CFArray? if errSecSuccess == SecCertificateCopyEmailAddresses(secCertificate, &emails) { self.emails = emails as? [String] } else { self.emails = nil } - + var secTrust: SecTrust? if errSecSuccess == SecTrustCreateWithCertificates(secCertificate, SecPolicyCreateBasicX509(), &secTrust), let certTrust = secTrust { if #available(iOS 14.0, macOS 11.0, *) { @@ -53,11 +53,11 @@ public struct ServerTrustViewModel: Encodable { publicKey = nil } } - + } - + struct SecKeyViewModel: Encodable { - + static func typeToString(_ type: String) -> String? { switch type as CFString { case kSecAttrKeyTypeRSA: return "RSA" @@ -66,14 +66,14 @@ public struct ServerTrustViewModel: Encodable { default: return nil } } - + let keyId: Data? let externalRepresentation: Data? - + let bitSize: Int? let blockSize: Int? let effectiveSize: Int? - + let canDecrypt: Bool let canDerive: Bool let canEncrypt: Bool @@ -81,20 +81,20 @@ public struct ServerTrustViewModel: Encodable { let canUnwrap: Bool let canVerify: Bool let canWrap: Bool - + let isPermanent: Bool? let type: String? - + init?(secKey: SecKey?) { guard let secKey = secKey else { return nil } - + blockSize = SecKeyGetBlockSize(secKey) externalRepresentation = SecKeyCopyExternalRepresentation(secKey, nil) as Data? - + let attrs: NSDictionary? = SecKeyCopyAttributes(secKey) - + bitSize = attrs?[kSecAttrKeySizeInBits] as? Int effectiveSize = attrs?[kSecAttrEffectiveKeySize] as? Int canDecrypt = attrs?[kSecAttrCanDecrypt] as? Bool ?? false @@ -106,36 +106,36 @@ public struct ServerTrustViewModel: Encodable { canWrap = attrs?[kSecAttrCanWrap] as? Bool ?? false isPermanent = attrs?[kSecAttrIsPermanent] as? Bool ?? false keyId = attrs?[kSecAttrApplicationLabel] as? Data - + if let type = attrs?[kSecAttrType] as? String { self.type = Self.typeToString(type) } else { self.type = nil } - + } - + } - + let secCertificateViewModels: [SecCertificateViewModel] - + public init?(serverTrust: SecTrust?) { guard let serverTrust = serverTrust else { return nil } - + let secTrust = serverTrust let count = SecTrustGetCertificateCount(secTrust) guard count != 0 else { return nil } - + var secCertificateViewModels = [SecCertificateViewModel]() for i in 0 ..< count { guard let certificate = SecTrustGetCertificateAtIndex(secTrust, i) else { return nil } let certificateViewModel = SecCertificateViewModel(secCertificate: certificate) secCertificateViewModels.append(certificateViewModel) } - + self.secCertificateViewModels = secCertificateViewModels } - + } diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapper.swift index ca61b4757..56caf5c64 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapper.swift @@ -1,6 +1,5 @@ // // JsonToRemoteConfigModelMapper.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift index ab3ce12a2..5a119c7c2 100644 --- a/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift +++ b/Sources/RemoteMessaging/Mappers/JsonToRemoteMessageModelMapper.swift @@ -1,6 +1,5 @@ // // JsonToRemoteMessageModelMapper.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift index 1e010cf9e..18a74e21a 100644 --- a/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/AppAttributeMatcher.swift @@ -1,6 +1,5 @@ // // AppAttributeMatcher.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -55,7 +54,7 @@ public struct AppAttributeMatcher: AttributeMatcher { guard let value = matchingAttribute.value else { return .fail } - + return BooleanMatchingAttribute(value).matches(value: isInternalUser) case let matchingAttribute as AppIdMatchingAttribute: guard let value = matchingAttribute.value, !value.isEmpty else { diff --git a/Sources/RemoteMessaging/Matchers/AttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/AttributeMatcher.swift index 42b21a639..e7ba2d3f6 100644 --- a/Sources/RemoteMessaging/Matchers/AttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/AttributeMatcher.swift @@ -1,6 +1,5 @@ // // AttributeMatcher.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift index 1a33d10d7..2dfa74b7b 100644 --- a/Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/DeviceAttributeMatcher.swift @@ -1,6 +1,5 @@ // // DeviceAttributeMatcher.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/Matchers/EvaluationResult.swift b/Sources/RemoteMessaging/Matchers/EvaluationResult.swift index d52d51dfc..fa17c2d71 100644 --- a/Sources/RemoteMessaging/Matchers/EvaluationResult.swift +++ b/Sources/RemoteMessaging/Matchers/EvaluationResult.swift @@ -1,6 +1,5 @@ // // EvaluationResult.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift index d3ecea04a..a4043560f 100644 --- a/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift +++ b/Sources/RemoteMessaging/Matchers/UserAttributeMatcher.swift @@ -1,6 +1,5 @@ // // UserAttributeMatcher.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/Model/AnyDecodable.swift b/Sources/RemoteMessaging/Model/AnyDecodable.swift index b419af128..385e934d6 100644 --- a/Sources/RemoteMessaging/Model/AnyDecodable.swift +++ b/Sources/RemoteMessaging/Model/AnyDecodable.swift @@ -32,15 +32,15 @@ import Foundation // swiftlint:disable type_name @usableFromInline -protocol _AnyDecodable { +protocol AnyDecodableProtocol { var value: Any { get } init(_ value: T?) } // swiftlint:enable type_name -extension AnyDecodable: _AnyDecodable {} +extension AnyDecodable: AnyDecodableProtocol {} -extension _AnyDecodable { +extension AnyDecodableProtocol { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() diff --git a/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift b/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift index 16336dbc6..1b73be27f 100644 --- a/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift +++ b/Sources/RemoteMessaging/Model/JsonRemoteMessagingConfig.swift @@ -1,6 +1,5 @@ // // JsonRemoteMessagingConfig.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -15,6 +14,7 @@ // 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 diff --git a/Sources/RemoteMessaging/Model/MatchingAttributes.swift b/Sources/RemoteMessaging/Model/MatchingAttributes.swift index 1934ac896..26419d465 100644 --- a/Sources/RemoteMessaging/Model/MatchingAttributes.swift +++ b/Sources/RemoteMessaging/Model/MatchingAttributes.swift @@ -1,6 +1,5 @@ // // MatchingAttributes.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -20,8 +19,6 @@ import Foundation import Common -// swiftlint:disable file_length - private enum RuleAttributes { static let min = "min" static let max = "max" diff --git a/Sources/RemoteMessaging/Model/RemoteConfigModel.swift b/Sources/RemoteMessaging/Model/RemoteConfigModel.swift index 9123b5b3d..a94021801 100644 --- a/Sources/RemoteMessaging/Model/RemoteConfigModel.swift +++ b/Sources/RemoteMessaging/Model/RemoteConfigModel.swift @@ -1,6 +1,5 @@ // // RemoteConfigModel.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/Model/RemoteMessageModel.swift b/Sources/RemoteMessaging/Model/RemoteMessageModel.swift index d149929b4..2bf6a39e0 100644 --- a/Sources/RemoteMessaging/Model/RemoteMessageModel.swift +++ b/Sources/RemoteMessaging/Model/RemoteMessageModel.swift @@ -1,6 +1,5 @@ // // RemoteMessageModel.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -83,7 +82,7 @@ public struct RemoteMessageModel: Equatable, Codable { placeholder: placeholder, actionText: translation.primaryActionText ?? actionText, action: action) - + } } } diff --git a/Sources/RemoteMessaging/Model/RemoteMessagingConfig.swift b/Sources/RemoteMessaging/Model/RemoteMessagingConfig.swift index 7aed17b04..f3e9bb6cd 100644 --- a/Sources/RemoteMessaging/Model/RemoteMessagingConfig.swift +++ b/Sources/RemoteMessaging/Model/RemoteMessagingConfig.swift @@ -1,6 +1,5 @@ // // RemoteMessagingConfig.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/RemoteMessagingConfigMatcher.swift b/Sources/RemoteMessaging/RemoteMessagingConfigMatcher.swift index 0d5682812..88e251d28 100644 --- a/Sources/RemoteMessaging/RemoteMessagingConfigMatcher.swift +++ b/Sources/RemoteMessaging/RemoteMessagingConfigMatcher.swift @@ -1,6 +1,5 @@ // // RemoteMessagingConfigMatcher.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/RemoteMessaging/RemoteMessagingConfigProcessor.swift b/Sources/RemoteMessaging/RemoteMessagingConfigProcessor.swift index df46f443e..233864732 100644 --- a/Sources/RemoteMessaging/RemoteMessagingConfigProcessor.swift +++ b/Sources/RemoteMessaging/RemoteMessagingConfigProcessor.swift @@ -1,6 +1,5 @@ // // RemoteMessagingConfigProcessor.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/SecureStorage/SecureStorageCryptoProvider.swift b/Sources/SecureStorage/SecureStorageCryptoProvider.swift index d4b41506a..9136b7c32 100644 --- a/Sources/SecureStorage/SecureStorageCryptoProvider.swift +++ b/Sources/SecureStorage/SecureStorageCryptoProvider.swift @@ -42,7 +42,7 @@ public protocol SecureStorageCryptoProvider { var keychainServiceName: String { get } - var keychainAccountName: String { get } + var keychainAccountName: String { get } } diff --git a/Sources/SecureStorage/SecureStorageError.swift b/Sources/SecureStorage/SecureStorageError.swift index b74e6da4a..1d482cf06 100644 --- a/Sources/SecureStorage/SecureStorageError.swift +++ b/Sources/SecureStorage/SecureStorageError.swift @@ -42,7 +42,7 @@ public enum SecureStorageError: Error { case duplicateRecord case keystoreError(status: Int32) case secError(status: Int32) - case generalCryptoError + case generalCryptoError case encodingFailed } diff --git a/Sources/SecureStorage/SecureVaultFactory.swift b/Sources/SecureStorage/SecureVaultFactory.swift index 8e75375a1..390242686 100644 --- a/Sources/SecureStorage/SecureVaultFactory.swift +++ b/Sources/SecureStorage/SecureVaultFactory.swift @@ -78,7 +78,7 @@ public class SecureVaultFactory { } } } - + public func makeSecureStorageProviders() throws -> SecureStorageProviders { let (cryptoProvider, keystoreProvider): (SecureStorageCryptoProvider, SecureStorageKeyStoreProvider) do { @@ -95,11 +95,11 @@ public class SecureVaultFactory { throw SecureStorageError.failedToOpenDatabase(cause: error) } } - + public func createAndInitializeEncryptionProviders() throws -> (SecureStorageCryptoProvider, SecureStorageKeyStoreProvider) { let cryptoProvider = makeCryptoProvider() let keystoreProvider = makeKeyStoreProvider() - + if try keystoreProvider.l1Key() != nil { return (cryptoProvider, keystoreProvider) } else { @@ -112,7 +112,7 @@ public class SecureVaultFactory { try keystoreProvider.storeEncryptedL2Key(encryptedL2Key) try keystoreProvider.storeGeneratedPassword(password) try keystoreProvider.storeL1Key(l1Key) - + return (cryptoProvider, keystoreProvider) } } diff --git a/Sources/SecureStorageTestsUtils/MockCryptoProvider.swift b/Sources/SecureStorageTestsUtils/MockCryptoProvider.swift index 7509a4fc9..5c15834fc 100644 --- a/Sources/SecureStorageTestsUtils/MockCryptoProvider.swift +++ b/Sources/SecureStorageTestsUtils/MockCryptoProvider.swift @@ -1,6 +1,5 @@ // // MockCryptoProvider.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SecureStorageTestsUtils/MockKeystoreProvider.swift b/Sources/SecureStorageTestsUtils/MockKeystoreProvider.swift index 12adec911..699b9a1af 100644 --- a/Sources/SecureStorageTestsUtils/MockKeystoreProvider.swift +++ b/Sources/SecureStorageTestsUtils/MockKeystoreProvider.swift @@ -1,6 +1,5 @@ // // MockKeystoreProvider.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SecureStorageTestsUtils/NoOpCryptoProvider.swift b/Sources/SecureStorageTestsUtils/NoOpCryptoProvider.swift index d6ef9dba7..e5b4285bf 100644 --- a/Sources/SecureStorageTestsUtils/NoOpCryptoProvider.swift +++ b/Sources/SecureStorageTestsUtils/NoOpCryptoProvider.swift @@ -1,6 +1,5 @@ // // NoOpCryptoProvider.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Bookmarks/internal/BookmarkEntity+Syncable.swift b/Sources/SyncDataProviders/Bookmarks/internal/BookmarkEntity+Syncable.swift index 4737d02df..1247d0df0 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/BookmarkEntity+Syncable.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/BookmarkEntity+Syncable.swift @@ -1,6 +1,5 @@ // // BookmarkEntity+Syncable.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift index 1fc79f9e9..09ff60ecb 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/BookmarksResponseHandler.swift @@ -1,6 +1,5 @@ // // BookmarksResponseHandler.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Bookmarks/internal/SyncableBookmarkAdapter.swift b/Sources/SyncDataProviders/Bookmarks/internal/SyncableBookmarkAdapter.swift index cd3c46c6a..742521cef 100644 --- a/Sources/SyncDataProviders/Bookmarks/internal/SyncableBookmarkAdapter.swift +++ b/Sources/SyncDataProviders/Bookmarks/internal/SyncableBookmarkAdapter.swift @@ -1,6 +1,5 @@ // // SyncableBookmarkAdapter.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Common/MetricsEvent.swift b/Sources/SyncDataProviders/Common/MetricsEvent.swift index 262a191bb..84a15dd79 100644 --- a/Sources/SyncDataProviders/Common/MetricsEvent.swift +++ b/Sources/SyncDataProviders/Common/MetricsEvent.swift @@ -1,6 +1,5 @@ // // MetricsEvent.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift index 0488328db..6b6ab139f 100644 --- a/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift +++ b/Sources/SyncDataProviders/Credentials/CredentialsProvider.swift @@ -1,6 +1,5 @@ // // CredentialsProvider.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -84,7 +83,7 @@ public final class CredentialsProvider: DataProvider { let syncableCredentials = try secureVault.modifiedSyncableCredentials() let encryptionKey = try crypter.fetchSecretKey() return try syncableCredentials.map { credentials in - try Syncable.init( + try Syncable( syncableCredentials: credentials, encryptedUsing: { try crypter.encryptAndBase64Encode($0, using: encryptionKey) } ) diff --git a/Sources/SyncDataProviders/Credentials/internal/CredentialsResponseHandler.swift b/Sources/SyncDataProviders/Credentials/internal/CredentialsResponseHandler.swift index 31164c437..451854e82 100644 --- a/Sources/SyncDataProviders/Credentials/internal/CredentialsResponseHandler.swift +++ b/Sources/SyncDataProviders/Credentials/internal/CredentialsResponseHandler.swift @@ -1,6 +1,5 @@ // // CredentialsResponseHandler.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Credentials/internal/SyncableCredentialsAdapter.swift b/Sources/SyncDataProviders/Credentials/internal/SyncableCredentialsAdapter.swift index 72bc4c05a..da0bad218 100644 --- a/Sources/SyncDataProviders/Credentials/internal/SyncableCredentialsAdapter.swift +++ b/Sources/SyncDataProviders/Credentials/internal/SyncableCredentialsAdapter.swift @@ -1,6 +1,5 @@ // // SyncableCredentialsAdapter.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Settings/SettingsProvider.swift b/Sources/SyncDataProviders/Settings/SettingsProvider.swift index 549d68549..3348f9be2 100644 --- a/Sources/SyncDataProviders/Settings/SettingsProvider.swift +++ b/Sources/SyncDataProviders/Settings/SettingsProvider.swift @@ -1,6 +1,5 @@ // // SettingsProvider.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -42,7 +41,6 @@ public struct SettingsSyncMetadataSaveError: Error { } } -// swiftlint:disable:next type_body_length public final class SettingsProvider: DataProvider, SettingSyncHandlingDelegate { public struct Setting: Hashable { diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailManager+SyncSupporting.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailManager+SyncSupporting.swift index 17923f1e9..4652f5141 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailManager+SyncSupporting.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailManager+SyncSupporting.swift @@ -1,6 +1,5 @@ // // EmailManager+SyncSupporting.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift index d1bb4181b..73dfcbff1 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/EmailProtectionSyncHandler.swift @@ -1,6 +1,5 @@ // // EmailProtectionSyncHandler.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/FavoritesDisplayModeSyncHandlerBase.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/FavoritesDisplayModeSyncHandlerBase.swift index 0414e1da3..bd5406d41 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/FavoritesDisplayModeSyncHandlerBase.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/FavoritesDisplayModeSyncHandlerBase.swift @@ -1,6 +1,5 @@ // // FavoritesDisplayModeSyncHandlerBase.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandler.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandler.swift index 5331d2767..3031bf435 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandler.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandler.swift @@ -1,6 +1,5 @@ // // SettingSyncHandler.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandling.swift b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandling.swift index 0d9948d6b..37d412a4f 100644 --- a/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandling.swift +++ b/Sources/SyncDataProviders/Settings/SettingsSyncHandlers/SettingSyncHandling.swift @@ -1,6 +1,5 @@ // // SettingSyncHandling.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Settings/internal/SettingsResponseHandler.swift b/Sources/SyncDataProviders/Settings/internal/SettingsResponseHandler.swift index 18dd57798..fd1e90c59 100644 --- a/Sources/SyncDataProviders/Settings/internal/SettingsResponseHandler.swift +++ b/Sources/SyncDataProviders/Settings/internal/SettingsResponseHandler.swift @@ -1,6 +1,5 @@ // // SettingsResponseHandler.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/SyncDataProviders/Settings/internal/SyncableSettingAdapter.swift b/Sources/SyncDataProviders/Settings/internal/SyncableSettingAdapter.swift index a16784629..8d86efcdf 100644 --- a/Sources/SyncDataProviders/Settings/internal/SyncableSettingAdapter.swift +++ b/Sources/SyncDataProviders/Settings/internal/SyncableSettingAdapter.swift @@ -1,6 +1,5 @@ // // SyncableSettingAdapter.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Sources/TestUtils/MockURLProtocol.swift b/Sources/TestUtils/MockURLProtocol.swift index 59a5ef5fe..103d2302b 100644 --- a/Sources/TestUtils/MockURLProtocol.swift +++ b/Sources/TestUtils/MockURLProtocol.swift @@ -1,6 +1,5 @@ // // MockURLProtocol.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,20 +20,20 @@ import Foundation /// A catch-all URL protocol that returns successful response and records all requests. final class MockURLProtocol: URLProtocol { - + static var lastRequest: URLRequest? static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data?))? - + override class func canInit(with request: URLRequest) -> Bool { true } - + override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } - + override func startLoading() { guard let handler = MockURLProtocol.requestHandler else { fatalError("Handler is unavailable.") } MockURLProtocol.lastRequest = request - + do { let (response, data) = try handler(request) client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) @@ -46,7 +45,7 @@ final class MockURLProtocol: URLProtocol { client?.urlProtocol(self, didFailWithError: error) } } - + override func stopLoading() { } - + } diff --git a/Sources/TestUtils/Utils/HTTPURLResponseExtension.swift b/Sources/TestUtils/Utils/HTTPURLResponseExtension.swift index 6414dbaff..9135b285e 100644 --- a/Sources/TestUtils/Utils/HTTPURLResponseExtension.swift +++ b/Sources/TestUtils/Utils/HTTPURLResponseExtension.swift @@ -1,6 +1,5 @@ // -// Configuration.swift -// DuckDuckGo +// HTTPURLResponseExtension.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,28 +20,28 @@ import Foundation @testable import Networking extension HTTPURLResponse { - + static let testEtag = "test-etag" static let testUrl = URL(string: "www.example.com")! - + static let ok = HTTPURLResponse(url: testUrl, statusCode: 200, httpVersion: nil, headerFields: [APIRequest.HTTPHeaderField.etag: testEtag])! - + static let okNoEtag = HTTPURLResponse(url: testUrl, statusCode: 200, httpVersion: nil, headerFields: [:])! - + static let notModified = HTTPURLResponse(url: testUrl, statusCode: 304, httpVersion: nil, headerFields: [APIRequest.HTTPHeaderField.etag: testEtag])! - + static let internalServerError = HTTPURLResponse(url: testUrl, statusCode: 500, httpVersion: nil, headerFields: [:])! - + } diff --git a/Sources/UserScript/StaticUserScript.swift b/Sources/UserScript/StaticUserScript.swift index 88a207444..425ab77c7 100644 --- a/Sources/UserScript/StaticUserScript.swift +++ b/Sources/UserScript/StaticUserScript.swift @@ -1,6 +1,5 @@ // -// UserScript.swift -// Core +// StaticUserScript.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/UserScript/UserScript.swift b/Sources/UserScript/UserScript.swift index 9319c0431..3b35ddc42 100644 --- a/Sources/UserScript/UserScript.swift +++ b/Sources/UserScript/UserScript.swift @@ -1,6 +1,5 @@ // // UserScript.swift -// Core // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/UserScript/UserScriptEncrypter.swift b/Sources/UserScript/UserScriptEncrypter.swift index d492eb32f..5391662fa 100644 --- a/Sources/UserScript/UserScriptEncrypter.swift +++ b/Sources/UserScript/UserScriptEncrypter.swift @@ -1,6 +1,5 @@ // -// AutofillUserScript+Encryption.swift -// DuckDuckGo +// UserScriptEncrypter.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/UserScript/UserScriptHostProvider.swift b/Sources/UserScript/UserScriptHostProvider.swift index 36b7955f7..a0d23bbec 100644 --- a/Sources/UserScript/UserScriptHostProvider.swift +++ b/Sources/UserScript/UserScriptHostProvider.swift @@ -1,6 +1,5 @@ // // UserScriptHostProvider.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Sources/UserScript/UserScriptMessage.swift b/Sources/UserScript/UserScriptMessage.swift index 29597fcde..01ff79458 100644 --- a/Sources/UserScript/UserScriptMessage.swift +++ b/Sources/UserScript/UserScriptMessage.swift @@ -1,6 +1,5 @@ // // UserScriptMessage.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -31,11 +30,11 @@ extension WKScriptMessage: UserScriptMessage { public var messageName: String { return name } - + public var messageBody: Any { return body } - + public var messageHost: String { return "\(frameInfo.securityOrigin.host)\(messagePort)" } @@ -47,7 +46,7 @@ extension WKScriptMessage: UserScriptMessage { public var isMainFrame: Bool { return frameInfo.isMainFrame } - + public var messageWebView: WKWebView? { return webView } diff --git a/Sources/UserScript/UserScriptMessageEncryption.swift b/Sources/UserScript/UserScriptMessageEncryption.swift index f41f35fc2..5f14ca0cc 100644 --- a/Sources/UserScript/UserScriptMessageEncryption.swift +++ b/Sources/UserScript/UserScriptMessageEncryption.swift @@ -1,6 +1,5 @@ // // UserScriptMessageEncryption.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Sources/UserScript/UserScriptMessaging.swift b/Sources/UserScript/UserScriptMessaging.swift index 8f7b91dba..bef522d0d 100644 --- a/Sources/UserScript/UserScriptMessaging.swift +++ b/Sources/UserScript/UserScriptMessaging.swift @@ -19,7 +19,7 @@ import Foundation import WebKit import Combine -import os.log +import Common /// A protocol to implement if you want to opt-in to centralised messaging. /// diff --git a/Sources/UserScript/UserScriptSourceProvider.swift b/Sources/UserScript/UserScriptSourceProvider.swift index 14d4ec5cb..193c920ca 100644 --- a/Sources/UserScript/UserScriptSourceProvider.swift +++ b/Sources/UserScript/UserScriptSourceProvider.swift @@ -1,6 +1,5 @@ // // UserScriptSourceProvider.swift -// Core // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml index 7c4c97660..bf8a5655d 100644 --- a/Tests/.swiftlint.yml +++ b/Tests/.swiftlint.yml @@ -1,10 +1,17 @@ disabled_rules: + - file_length + - unused_closure_parameter + - type_name - force_cast - force_try - - file_length - function_body_length - - function_parameter_count + - cyclomatic_complexity - identifier_name - - line_length + - blanket_disable_command - type_body_length - - type_name + - explicit_non_final_class + - enforce_os_log_wrapper + +large_tuple: + warning: 6 + error: 10 diff --git a/Tests/BookmarksTests/BookmarkDatabaseCleanerTests.swift b/Tests/BookmarksTests/BookmarkDatabaseCleanerTests.swift index 11e8e1971..2d49d21a9 100644 --- a/Tests/BookmarksTests/BookmarkDatabaseCleanerTests.swift +++ b/Tests/BookmarksTests/BookmarkDatabaseCleanerTests.swift @@ -1,6 +1,5 @@ // // BookmarkDatabaseCleanerTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BookmarksTests/BookmarkEntityTests.swift b/Tests/BookmarksTests/BookmarkEntityTests.swift index 7583c758a..4bc5ef218 100644 --- a/Tests/BookmarksTests/BookmarkEntityTests.swift +++ b/Tests/BookmarksTests/BookmarkEntityTests.swift @@ -1,6 +1,5 @@ // // BookmarkEntityTests.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // diff --git a/Tests/BookmarksTests/BookmarkListViewModelTests.swift b/Tests/BookmarksTests/BookmarkListViewModelTests.swift index 19d6ffeb8..3966b53b2 100644 --- a/Tests/BookmarksTests/BookmarkListViewModelTests.swift +++ b/Tests/BookmarksTests/BookmarkListViewModelTests.swift @@ -1,6 +1,5 @@ // // BookmarkListViewModelTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -95,7 +94,7 @@ final class BookmarkListViewModelTests: XCTestCase { context.performAndWait { bookmarkTree.createEntities(in: context) - + try! context.save() let bookmark = BookmarkEntity.fetchBookmark(withUUID: "2", context: context)! diff --git a/Tests/BookmarksTests/BookmarkMigrationTests.swift b/Tests/BookmarksTests/BookmarkMigrationTests.swift index baae0fb03..15b88f1b7 100644 --- a/Tests/BookmarksTests/BookmarkMigrationTests.swift +++ b/Tests/BookmarksTests/BookmarkMigrationTests.swift @@ -1,6 +1,5 @@ // // BookmarkMigrationTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -141,7 +140,7 @@ class BookmarkMigrationTests: XCTestCase { XCTFail("Failed to load model") return } - + let context = bookmarksDatabase.makeContext(concurrencyType: .privateQueueConcurrencyType) context.performAndWait { diff --git a/Tests/BookmarksTests/BookmarkUtilsTests.swift b/Tests/BookmarksTests/BookmarkUtilsTests.swift index 3cd9637cf..37f8397f9 100644 --- a/Tests/BookmarksTests/BookmarkUtilsTests.swift +++ b/Tests/BookmarksTests/BookmarkUtilsTests.swift @@ -1,6 +1,5 @@ // // BookmarkUtilsTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BookmarksTests/FaviconsFetcher/BookmarkDomainsTests.swift b/Tests/BookmarksTests/FaviconsFetcher/BookmarkDomainsTests.swift index 02250ca6e..18d74f8a5 100644 --- a/Tests/BookmarksTests/FaviconsFetcher/BookmarkDomainsTests.swift +++ b/Tests/BookmarksTests/FaviconsFetcher/BookmarkDomainsTests.swift @@ -1,6 +1,5 @@ // // BookmarkDomainsTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BookmarksTests/FaviconsFetcher/BookmarksFaviconsFetcherTests.swift b/Tests/BookmarksTests/FaviconsFetcher/BookmarksFaviconsFetcherTests.swift index 47c89e74d..e1780a37e 100644 --- a/Tests/BookmarksTests/FaviconsFetcher/BookmarksFaviconsFetcherTests.swift +++ b/Tests/BookmarksTests/FaviconsFetcher/BookmarksFaviconsFetcherTests.swift @@ -1,6 +1,5 @@ // // BookmarksFaviconsFetcherTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetchOperationTests.swift b/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetchOperationTests.swift index 2da747b28..66a0690a5 100644 --- a/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetchOperationTests.swift +++ b/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetchOperationTests.swift @@ -1,6 +1,5 @@ // // FaviconsFetchOperationTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetcherMocks.swift b/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetcherMocks.swift index 7664c95a7..18c9fb2a9 100644 --- a/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetcherMocks.swift +++ b/Tests/BookmarksTests/FaviconsFetcher/FaviconsFetcherMocks.swift @@ -1,6 +1,5 @@ // // FaviconsFetcherMocks.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BookmarksTests/FavoriteListViewModelTests.swift b/Tests/BookmarksTests/FavoriteListViewModelTests.swift index f01cb2e21..a772435b3 100644 --- a/Tests/BookmarksTests/FavoriteListViewModelTests.swift +++ b/Tests/BookmarksTests/FavoriteListViewModelTests.swift @@ -1,6 +1,5 @@ // // FavoriteListViewModelTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/Autofill/AutofillEmailUserScriptTests.swift b/Tests/BrowserServicesKitTests/Autofill/AutofillEmailUserScriptTests.swift index 03363bafe..b9897c2e0 100644 --- a/Tests/BrowserServicesKitTests/Autofill/AutofillEmailUserScriptTests.swift +++ b/Tests/BrowserServicesKitTests/Autofill/AutofillEmailUserScriptTests.swift @@ -1,6 +1,5 @@ // // AutofillEmailUserScriptTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -85,11 +84,11 @@ class AutofillEmailUserScriptTests: XCTestCase { func testWhenReceivesStoreTokenMessageThenCallsDelegateMethodWithCorrectTokenAndUsername() { let mock = MockAutofillEmailDelegate() userScript.emailDelegate = mock - + let token = "testToken" let username = "testUsername" let cohort = "testCohort" - + let expect = expectation(description: "testWhenReceivesStoreTokenMessageThenCallsDelegateMethod") mock.requestStoreTokenCallback = { callbackToken, callbackUsername, callbackCohort in XCTAssertEqual(token, callbackToken) @@ -125,11 +124,11 @@ class AutofillEmailUserScriptTests: XCTestCase { waitForExpectations(timeout: 1.0, handler: nil) } - + func testWhenReceivesGetAliasMessageThenCallsDelegateMethod() { let mock = MockAutofillEmailDelegate() userScript.emailDelegate = mock - + let expect = expectation(description: "testWhenReceivesGetAliasMessageThenCallsDelegateMethod") mock.requestAliasCallback = { expect.fulfill() @@ -147,11 +146,11 @@ class AutofillEmailUserScriptTests: XCTestCase { XCTAssertNotNil(mockWebView.javaScriptString) } - + func testWhenReceivesRefreshAliasMessageThenCallsDelegateMethod() { let mock = MockAutofillEmailDelegate() userScript.emailDelegate = mock - + let expect = expectation(description: "testWhenReceivesRefreshAliasMessageThenCallsDelegateMethod") mock.refreshAliasCallback = { expect.fulfill() @@ -207,15 +206,15 @@ class AutofillEmailUserScriptTests: XCTestCase { } class MockWKScriptMessage: WKScriptMessage { - + let mockedName: String let mockedBody: Any let mockedWebView: WKWebView? - + override var name: String { return mockedName } - + override var body: Any { return mockedBody } @@ -223,7 +222,7 @@ class MockWKScriptMessage: WKScriptMessage { override var webView: WKWebView? { return mockedWebView } - + init(name: String, body: Any, webView: WKWebView? = nil) { self.mockedName = name self.mockedBody = body @@ -233,7 +232,7 @@ class MockWKScriptMessage: WKScriptMessage { } class MockUserScriptMessage: UserScriptMessage { - + let mockedName: String let mockedBody: Any let mockedHost: String @@ -243,11 +242,11 @@ class MockUserScriptMessage: UserScriptMessage { var isMainFrame: Bool { return mockedMainFrame } - + var messageName: String { return mockedName } - + var messageBody: Any { return mockedBody } @@ -255,11 +254,11 @@ class MockUserScriptMessage: UserScriptMessage { var messageWebView: WKWebView? { return mockedWebView } - + var messageHost: String { return mockedHost } - + init(name: String, body: Any, host: String, webView: WKWebView? = nil) { self.mockedName = name self.mockedBody = body @@ -298,7 +297,7 @@ class MockAutofillEmailDelegate: AutofillEmailDelegate { signedInCallback?() return false } - + func autofillUserScript(_: AutofillUserScript, didRequestAliasAndRequiresUserPermission requiresUserPermission: Bool, shouldConsumeAliasIfProvided: Bool, @@ -306,11 +305,11 @@ class MockAutofillEmailDelegate: AutofillEmailDelegate { requestAliasCallback?() completionHandler("alias", true, nil) } - + func autofillUserScriptDidRequestRefreshAlias(_: AutofillUserScript) { refreshAliasCallback?() } - + func autofillUserScript(_: AutofillUserScript, didRequestStoreToken token: String, username: String, cohort: String?) { requestStoreTokenCallback!(token, username, cohort) } diff --git a/Tests/BrowserServicesKitTests/Autofill/AutofillTestHelper.swift b/Tests/BrowserServicesKitTests/Autofill/AutofillTestHelper.swift index 529a680c9..67e825e89 100644 --- a/Tests/BrowserServicesKitTests/Autofill/AutofillTestHelper.swift +++ b/Tests/BrowserServicesKitTests/Autofill/AutofillTestHelper.swift @@ -1,6 +1,5 @@ // // AutofillTestHelper.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -24,7 +23,7 @@ import BrowserServicesKit import TrackerRadarKit struct AutofillTestHelper { - + static func preparePrivacyConfig(embeddedConfig: Data) -> PrivacyConfigurationManager { let mockEmbeddedData = MockEmbeddedDataProvider(data: embeddedConfig, etag: "embedded") diff --git a/Tests/BrowserServicesKitTests/Autofill/AutofillUserScriptSourceProviderTests.swift b/Tests/BrowserServicesKitTests/Autofill/AutofillUserScriptSourceProviderTests.swift index 5f0fc7e57..0668c9442 100644 --- a/Tests/BrowserServicesKitTests/Autofill/AutofillUserScriptSourceProviderTests.swift +++ b/Tests/BrowserServicesKitTests/Autofill/AutofillUserScriptSourceProviderTests.swift @@ -1,6 +1,5 @@ // // AutofillUserScriptSourceProviderTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/Autofill/AutofillVaultUserScriptTests.swift b/Tests/BrowserServicesKitTests/Autofill/AutofillVaultUserScriptTests.swift index 5f1e54f1e..5cdf54bc1 100644 --- a/Tests/BrowserServicesKitTests/Autofill/AutofillVaultUserScriptTests.swift +++ b/Tests/BrowserServicesKitTests/Autofill/AutofillVaultUserScriptTests.swift @@ -1,6 +1,5 @@ // // AutofillVaultUserScriptTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -215,7 +214,7 @@ class AutofillVaultUserScriptTests: XCTestCase { let randomAccountId = Int.random(in: 0 ..< Int.max) // JS will come through as a Int rather than Int64 hostProvider = MockHostProvider(host: "www.domain1.com") - + let delegate = GetCredentialsDelegate() delegate.tld = tld userScript.vaultDelegate = delegate @@ -460,7 +459,7 @@ class AutofillVaultUserScriptTests: XCTestCase { XCTAssertEqual(delegate.lastDomain, "example.com") } - + func testWhenInitializingAutofillData_WhenCredentialsAreProvidedWithoutAUsername_ThenAutofillDataIsStillInitialized() { let password = "password" let detectedAutofillData = [ @@ -468,34 +467,34 @@ class AutofillVaultUserScriptTests: XCTestCase { "password": password ] ] - + let autofillData = AutofillUserScript.DetectedAutofillData(dictionary: detectedAutofillData) - + XCTAssertNil(autofillData.creditCard) XCTAssertNil(autofillData.identity) XCTAssertNotNil(autofillData.credentials) - + XCTAssertEqual(autofillData.credentials?.username, nil) XCTAssertEqual(autofillData.credentials?.password, password) } - + func testWhenInitializingAutofillData_WhenCredentialsAreProvidedWithAUsername_ThenAutofillDataIsStillInitialized() { let username = "username" let password = "password" - + let detectedAutofillData = [ "credentials": [ "username": username, "password": password ] ] - + let autofillData = AutofillUserScript.DetectedAutofillData(dictionary: detectedAutofillData) - + XCTAssertNil(autofillData.creditCard) XCTAssertNil(autofillData.identity) XCTAssertNotNil(autofillData.credentials) - + XCTAssertEqual(autofillData.credentials?.username, username) XCTAssertEqual(autofillData.credentials?.password, password) } @@ -518,11 +517,11 @@ class AutofillVaultUserScriptTests: XCTestCase { let predicate = NSPredicate(block: { _, _ -> Bool in return !delegate.receivedCallbacks.isEmpty }) - + let expectation = XCTNSPredicateExpectation(predicate: predicate, object: delegate.receivedCallbacks) - + wait(for: [expectation], timeout: 5) - + XCTAssertEqual(delegate.lastSubtype, AutofillUserScript.GetAutofillDataSubType.username) } @@ -574,7 +573,7 @@ class MockSecureVaultDelegate: AutofillSecureVaultDelegate { } var receivedCallbacks: [CallbackType] = [] - + var lastDomain: String? var lastUsername: String? var lastPassword: String? @@ -644,7 +643,7 @@ class MockSecureVaultDelegate: AutofillSecureVaultDelegate { lastSubtype = subType receivedCallbacks.append(.didRequestCredentialsForDomain) let provider = SecureVaultModels.CredentialsProvider(name: .duckduckgo, locked: false) - + completionHandler(nil, provider, .none) } @@ -659,7 +658,7 @@ class MockSecureVaultDelegate: AutofillSecureVaultDelegate { func autofillUserScriptDidOfferGeneratedPassword(_: BrowserServicesKit.AutofillUserScript, password: String, completionHandler: @escaping (Bool) -> Void) { } - + func autofillUserScript(_: AutofillUserScript, didSendPixel pixel: AutofillUserScript.JSPixel) { } } diff --git a/Tests/BrowserServicesKitTests/Autofill/Matchers/AutofillDomainNameUrlMatcherTests.swift b/Tests/BrowserServicesKitTests/Autofill/Matchers/AutofillDomainNameUrlMatcherTests.swift index d22275dff..ffd7f7d84 100644 --- a/Tests/BrowserServicesKitTests/Autofill/Matchers/AutofillDomainNameUrlMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/Autofill/Matchers/AutofillDomainNameUrlMatcherTests.swift @@ -1,6 +1,5 @@ // // AutofillDomainNameUrlMatcherTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/Autofill/Matchers/AutofillWebsiteAccountMatcherTests.swift b/Tests/BrowserServicesKitTests/Autofill/Matchers/AutofillWebsiteAccountMatcherTests.swift index 035b7238b..b16380a1d 100644 --- a/Tests/BrowserServicesKitTests/Autofill/Matchers/AutofillWebsiteAccountMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/Autofill/Matchers/AutofillWebsiteAccountMatcherTests.swift @@ -1,6 +1,5 @@ // -// AutofillDomainNameUrlGrouperTests.swift -// DuckDuckGo +// AutofillWebsiteAccountMatcherTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/Autofill/Sort/AutofillDomainNameUrlSortTests.swift b/Tests/BrowserServicesKitTests/Autofill/Sort/AutofillDomainNameUrlSortTests.swift index 2eab9eb42..a2b51f695 100644 --- a/Tests/BrowserServicesKitTests/Autofill/Sort/AutofillDomainNameUrlSortTests.swift +++ b/Tests/BrowserServicesKitTests/Autofill/Sort/AutofillDomainNameUrlSortTests.swift @@ -1,6 +1,5 @@ // // AutofillDomainNameUrlSortTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionCounterTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionCounterTests.swift index f72f0ee93..017050911 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionCounterTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionCounterTests.swift @@ -1,6 +1,5 @@ // // AdClickAttributionCounterTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,9 +21,9 @@ import Persistence @testable import BrowserServicesKit class MockKeyValueStore: KeyValueStoring { - + var store = [String: Any?]() - + func object(forKey defaultName: String) -> Any? { return store[defaultName] as Any? } @@ -36,7 +35,7 @@ class MockKeyValueStore: KeyValueStoring { func removeObject(forKey defaultName: String) { store[defaultName] = nil } - + } class AdClickAttributionCounterTests: XCTestCase { @@ -46,62 +45,62 @@ class AdClickAttributionCounterTests: XCTestCase { let counter = AdClickAttributionCounter(store: mockStore, onSendRequest: { _ in XCTFail("Should not send anything") }) - + let date = Date() // First use saves date if not present in store counter.onAttributionActive(currentTime: date) - + // Second use, later, but before sync interval counter.onAttributionActive(currentTime: date + 1) - + let count = mockStore.object(forKey: AdClickAttributionCounter.Constant.pageLoadsCountKey) as? Int XCTAssertEqual(count, 2) - + let storedDate = mockStore.object(forKey: AdClickAttributionCounter.Constant.lastSendAtKey) as? Date XCTAssertEqual(date, storedDate) } - + var onSend: (Int) -> Void = { _ in } - + func testWhenTimeIntervalHasPassedThenDataIsSent() { let interval: Double = 60 * 60 - + let expectation = expectation(description: "Data sent") expectation.expectedFulfillmentCount = 2 - + let mockStore = MockKeyValueStore() let counter = AdClickAttributionCounter(store: mockStore, sendInterval: interval) { count in self.onSend(count) } - + onSend = { _ in XCTFail("Send not expected") } - + counter.onAttributionActive() counter.onAttributionActive() counter.onAttributionActive(currentTime: Date() + interval - 1) - + counter.sendEventsIfNeeded() - + onSend = { count in expectation.fulfill() XCTAssertEqual(count, 3) } - + // timestamp in counter will become now + interval counter.sendEventsIfNeeded(currentTime: Date() + interval + 1) - + onSend = { _ in XCTFail("Send not expected") } - + counter.onAttributionActive(currentTime: Date() + interval + 1) - + onSend = { count in expectation.fulfill() XCTAssertEqual(count, 2) } - + // Add another interval to trigger sync counter.onAttributionActive(currentTime: Date() + 2*interval + 1) - + waitForExpectations(timeout: 1, handler: nil) } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionDetectionTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionDetectionTests.swift index 2aeccf2cf..c3739ddb8 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionDetectionTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionDetectionTests.swift @@ -1,6 +1,5 @@ // // AdClickAttributionDetectionTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,42 +21,42 @@ import BrowserServicesKit import Common final class MockAttributing: AdClickAttributing { - + init(onFormatMatching: @escaping (URL) -> Bool = { _ in return true }, onParameterNameQuery: @escaping (URL) -> String? = { _ in return nil }) { self.onFormatMatching = onFormatMatching self.onParameterNameQuery = onParameterNameQuery } - + var isEnabled = true - + var allowlist = [AdClickAttributionFeature.AllowlistEntry]() - + var navigationExpiration: Double = 30 var totalExpiration: Double = 7 * 24 * 60 - + var onFormatMatching: (URL) -> Bool var onParameterNameQuery: (URL) -> String? - + func isMatchingAttributionFormat(_ url: URL) -> Bool { return onFormatMatching(url) } - + func attributionDomainParameterName(for url: URL) -> String? { return onParameterNameQuery(url) } - + var isHeuristicDetectionEnabled: Bool = true var isDomainDetectionEnabled: Bool = true - + } final class MockAdClickAttributionDetectionDelegate: AdClickAttributionDetectionDelegate { - + init(onAttributionDetection: @escaping (String) -> Void) { self.onAttributionDetection = onAttributionDetection } - + var onAttributionDetection: (String) -> Void func attributionDetection(_ detection: AdClickAttributionDetection, didDetectVendor vendorHost: String) { onAttributionDetection(vendorHost) @@ -65,240 +64,240 @@ final class MockAdClickAttributionDetectionDelegate: AdClickAttributionDetection } final class AdClickAttributionDetectionTests: XCTestCase { - + let domainParameterName = "ad_domain_param.com" - + static let tld = TLD() - + func testWhenFeatureIsDisabledThenNothingIsDetected() { let feature = MockAttributing { _ in return true } feature.isEnabled = false - + let delegate = MockAdClickAttributionDetectionDelegate { _ in XCTFail("Nothing should be detected") } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com")) detection.on2XXResponse(url: URL(string: "https://test.com")) detection.onDidFinishNavigation(url: URL(string: "https://test.com")) } - + func testWhenHeuristicOptionIsDisabledThenNothingIsDetected() { let feature = MockAttributing { _ in return true } feature.isHeuristicDetectionEnabled = false - + let delegate = MockAdClickAttributionDetectionDelegate { _ in XCTFail("Nothing should be detected") } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com")) detection.on2XXResponse(url: URL(string: "https://test.com")) detection.onDidFinishNavigation(url: URL(string: "https://test.com")) } - + func testWhenDomainDetectionOptionIsDisabledThenFallbackToHeuristic() { let feature = MockAttributing(onParameterNameQuery: { _ in return self.domainParameterName }) feature.isDomainDetectionEnabled = false feature.isHeuristicDetectionEnabled = true - + var delegate = MockAdClickAttributionDetectionDelegate { _ in XCTFail("Nothing should be detected") } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com?\(domainParameterName)=domain.net")) - + let delegateCalled = expectation(description: "Delegate called") delegate = MockAdClickAttributionDetectionDelegate { vendorHost in XCTAssertEqual(vendorHost, "test.com") delegateCalled.fulfill() } detection.delegate = delegate - + detection.on2XXResponse(url: URL(string: "https://test.com")) detection.onDidFinishNavigation(url: URL(string: "https://test.com")) - + wait(for: [delegateCalled], timeout: 0.1) } - + func testWhenThereAreNoMatchesThenNothingIsDetected() { - + let feature = MockAttributing { _ in return false } - + let delegate = MockAdClickAttributionDetectionDelegate { _ in XCTFail("Nothing should be detected") } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com")) detection.on2XXResponse(url: URL(string: "https://test.com")) detection.onDidFinishNavigation(url: URL(string: "https://test.com")) } - + func testWhenThereAreMatchesThenVendorIsDetected_Heuristic() { - + let feature = MockAttributing { _ in return true } - + let delegateCalled = expectation(description: "Delegate called") - + let delegate = MockAdClickAttributionDetectionDelegate { vendorHost in XCTAssertEqual(vendorHost, "test.com") delegateCalled.fulfill() } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com")) detection.on2XXResponse(url: URL(string: "https://test.com")) detection.onDidFinishNavigation(url: URL(string: "https://test.com")) - + waitForExpectations(timeout: 0.1) } - + func testWhenThereAreMatchesThenVendorIsDetected_Directly() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return self.domainParameterName }) - + let delegateCalled = expectation(description: "Delegate called") - + let delegate = MockAdClickAttributionDetectionDelegate { vendorHost in XCTAssertEqual(vendorHost, "domain.net") delegateCalled.fulfill() } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com?\(domainParameterName)=domain.net")) detection.on2XXResponse(url: URL(string: "https://test.com")) detection.onDidFinishNavigation(url: URL(string: "https://test.com")) - + waitForExpectations(timeout: 0.1) } - + func testWhenThereAreMatchesThenVendorIsETLDplus1_Heuristic() { - + let feature = MockAttributing { _ in return true } - + let delegateCalled = expectation(description: "Delegate called") - + let delegate = MockAdClickAttributionDetectionDelegate { vendorHost in XCTAssertEqual(vendorHost, "test.com") delegateCalled.fulfill() } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com")) detection.on2XXResponse(url: URL(string: "https://a.sub.test.com")) detection.onDidFinishNavigation(url: URL(string: "https://a.sub.test.com")) - + waitForExpectations(timeout: 0.1) } - + func testWhenThereAreMatchesThenVendorIsETLDplus1_Directly() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return self.domainParameterName }) - + let delegateCalled = expectation(description: "Delegate called") - + let delegate = MockAdClickAttributionDetectionDelegate { vendorHost in XCTAssertEqual(vendorHost, "domain.net") delegateCalled.fulfill() } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com?\(domainParameterName)=a.domain.net")) detection.on2XXResponse(url: URL(string: "https://sub.test.com")) detection.onDidFinishNavigation(url: URL(string: "https://sub.test.com")) - + waitForExpectations(timeout: 0.1) } - + func testWhenMatchedAndWrongParameterThenFallbackToHeuristic() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return self.domainParameterName }) - + let delegateCalled = expectation(description: "Delegate called") delegateCalled.expectedFulfillmentCount = 1 - + let delegate = MockAdClickAttributionDetectionDelegate { vendorHost in XCTAssertEqual(vendorHost, "test.com") delegateCalled.fulfill() } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://example.com?\(domainParameterName)=com")) detection.on2XXResponse(url: URL(string: "https://sub.test.com")) - + // Should match and notify only once detection.on2XXResponse(url: URL(string: "https://another.test.com")) - + detection.onDidFinishNavigation(url: URL(string: "https://another.test.com")) - + waitForExpectations(timeout: 0.1) } - + func testWhenNavigationFailsThenCorrectVendorIsDetected() { - + let feature = MockAttributing { _ in return true } - + var delegate = MockAdClickAttributionDetectionDelegate { _ in XCTFail("Should not detect in case of an error") } - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld) detection.delegate = delegate - + // First matching requests that fails detection.onStartNavigation(url: URL(string: "https://example.com")) detection.onDidFailNavigation() - + // Simulate non-matching request - nothing should be detected feature.onFormatMatching = { _ in return false } - + detection.onStartNavigation(url: URL(string: "https://other.com")) detection.on2XXResponse(url: URL(string: "https://test.com")) detection.onDidFinishNavigation(url: URL(string: "https://test.com")) - + // Simulate matching request - it should be detected feature.onFormatMatching = { _ in return true } - + let delegateCalled = expectation(description: "Delegate called") delegate = MockAdClickAttributionDetectionDelegate { vendorHost in XCTAssertEqual(vendorHost, "something.com") delegateCalled.fulfill() } detection.delegate = delegate - + detection.onStartNavigation(url: URL(string: "https://domain.com")) detection.on2XXResponse(url: URL(string: "https://a.something.com")) detection.onDidFinishNavigation(url: URL(string: "https://a.something.com")) - + waitForExpectations(timeout: 0.1) } } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionLogicTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionLogicTests.swift index 3deb8cd38..fa5c355fb 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionLogicTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionLogicTests.swift @@ -1,6 +1,5 @@ // // AdClickAttributionLogicTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -23,35 +22,35 @@ import Common @testable import BrowserServicesKit final class MockAttributionRulesProvider: AdClickAttributionRulesProviding { - + enum Constants { static let globalAttributionRulesListName = "global" } - + init() async { globalAttributionRules = await ContentBlockingRulesHelper().makeFakeRules(name: Constants.globalAttributionRulesListName, tdsEtag: "tdsEtag", tempListId: "tempEtag", allowListId: nil, unprotectedSitesHash: nil) - + XCTAssertNotNil(globalAttributionRules) } - + var globalAttributionRules: ContentBlockerRulesManager.Rules? - + var onRequestingAttribution: (String, @escaping (ContentBlockerRulesManager.Rules?) -> Void) -> Void = { _, _ in } func requestAttribution(forVendor vendor: String, completion: @escaping (ContentBlockerRulesManager.Rules?) -> Void) { onRequestingAttribution(vendor, completion) } - + } final class MockAdClickAttributionLogicDelegate: AdClickAttributionLogicDelegate { - + var onRequestingRuleApplication: (ContentBlockerRulesManager.Rules?) -> Void = { _ in } - + func attributionLogic(_ logic: AdClickAttributionLogic, didRequestRuleApplication rules: ContentBlockerRulesManager.Rules?, forVendor vendor: String?) { @@ -61,49 +60,49 @@ final class MockAdClickAttributionLogicDelegate: AdClickAttributionLogicDelegate // swiftlint:disable weak_delegate final class AdClickAttributionLogicTests: XCTestCase { - + static let tld = TLD() - + let feature = MockAttributing() let mockDelegate = MockAdClickAttributionLogicDelegate() - + func testWhenInitializedThenGlobalRulesApplied() async { - + let mockRulesProvider = await MockAttributionRulesProvider() - + let logic = AdClickAttributionLogic(featureConfig: feature, rulesProvider: mockRulesProvider, tld: Self.tld) - + logic.delegate = mockDelegate - + let rulesApplied = expectation(description: "Rules Applied") mockDelegate.onRequestingRuleApplication = { rules in XCTAssertNotNil(rules) XCTAssertEqual(rules?.name, MockAttributionRulesProvider.Constants.globalAttributionRulesListName) rulesApplied.fulfill() } - + logic.onRulesChanged(latestRules: [mockRulesProvider.globalAttributionRules!]) await fulfillment(of: [rulesApplied], timeout: 0.1) } - + func testWhenAttributionDetectedThenNewRulesAreRequestedAndApplied() async { - + let mockAttributedRules = await ContentBlockingRulesHelper().makeFakeRules(name: "attributed") let mockDetection = AdClickAttributionDetection(feature: feature, tld: Self.tld) - + let mockRulesProvider = await MockAttributionRulesProvider() - + let logic = AdClickAttributionLogic(featureConfig: feature, rulesProvider: mockRulesProvider, tld: Self.tld) - + logic.delegate = mockDelegate logic.onRulesChanged(latestRules: [mockRulesProvider.globalAttributionRules!]) - + // Regular navigation, call handler immediately let navigationAllowed = expectation(description: "Navigation allowed") logic.onProvisionalNavigation { navigationAllowed.fulfill() } @@ -111,23 +110,23 @@ final class AdClickAttributionLogicTests: XCTestCase { // Expect // 1. Call to request attribution for found vendor - + var attributedRulesPrepared: (ContentBlockerRulesManager.Rules?) -> Void = { _ in XCTFail("Expected actual handler") } mockRulesProvider.onRequestingAttribution = { vendor, completion in XCTAssertEqual(vendor, "example.com") attributedRulesPrepared = completion } - + logic.attributionDetection(mockDetection, didDetectVendor: "example.com") - + // 2. Wait with N requests till rules are ready. var requestCompletedCount = 0 logic.onProvisionalNavigation { requestCompletedCount += 1 } logic.onProvisionalNavigation { requestCompletedCount += 1 } - + // Nothing completed yet... XCTAssertEqual(requestCompletedCount, 0) - + // 3. Apply rules once ready (when callback is called) let rulesApplied = expectation(description: "Rules Applied") mockDelegate.onRequestingRuleApplication = { rules in @@ -135,41 +134,41 @@ final class AdClickAttributionLogicTests: XCTestCase { XCTAssertEqual(rules?.name, mockAttributedRules?.name) rulesApplied.fulfill() } - + // 4. Expect navigation to happen once rules are prepared attributedRulesPrepared(mockAttributedRules) - + // Requests completed now XCTAssertEqual(requestCompletedCount, 2) await fulfillment(of: [rulesApplied], timeout: 0.5) } - + func testWhenAttributionDetectedThenPreviousOneIsReplaced() async { - + let mockDetection = AdClickAttributionDetection(feature: feature, tld: Self.tld) let mockRulesProvider = await MockAttributionRulesProvider() - + let mockAttributedRules = await ContentBlockingRulesHelper().makeFakeRules(name: "attributed") - + let logic = AdClickAttributionLogic(featureConfig: feature, rulesProvider: mockRulesProvider, tld: Self.tld) - + logic.delegate = mockDelegate logic.onRulesChanged(latestRules: [mockRulesProvider.globalAttributionRules!]) logic.onProvisionalNavigation { } - + // Expect // 1. Call to request attribution for found vendor - + // - Mock rules creation mockRulesProvider.onRequestingAttribution = { vendor, completion in XCTAssertEqual(vendor, "example.com") completion(mockAttributedRules) } - + let rulesApplied = expectation(description: "Rules Applied") mockDelegate.onRequestingRuleApplication = { rules in XCTAssertNotNil(rules?.name) @@ -177,27 +176,27 @@ final class AdClickAttributionLogicTests: XCTestCase { rulesApplied.fulfill() } // - - + logic.attributionDetection(mockDetection, didDetectVendor: "example.com") await fulfillment(of: [rulesApplied], timeout: 0.2) - + // 2. These should be executed immediately var requestCompletedCount = 0 logic.onProvisionalNavigation { requestCompletedCount += 1 } logic.onProvisionalNavigation { requestCompletedCount += 1 } - + XCTAssertEqual(requestCompletedCount, 2) - + logic.onDidFinishNavigation(host: "test.com") - + // - Mock new rules creation let mockNewAttributedRules = await ContentBlockingRulesHelper().makeFakeRules(name: "newAttributed") - + mockRulesProvider.onRequestingAttribution = { vendor, completion in XCTAssertEqual(vendor, "other.com") completion(mockNewAttributedRules) } - + let newRulesApplied = expectation(description: "New Rules Applied") mockDelegate.onRequestingRuleApplication = { rules in XCTAssertNotNil(rules?.name) @@ -205,26 +204,26 @@ final class AdClickAttributionLogicTests: XCTestCase { newRulesApplied.fulfill() } // - - + // 3. Simulate new navigation. logic.onProvisionalNavigation { requestCompletedCount += 1 } // 4. And new attribution detection. logic.attributionDetection(mockDetection, didDetectVendor: "other.com") await fulfillment(of: [newRulesApplied], timeout: 0.2) - + logic.onProvisionalNavigation { requestCompletedCount += 1 } logic.onProvisionalNavigation { requestCompletedCount += 1 } - + // Requests completed now XCTAssertEqual(requestCompletedCount, 5) } } final class AdClickAttributionLogicHelper { - + static let tld = TLD() static let feature = MockAttributing() - + static func prepareLogic(attributedVendorHost: String, eventReporting: EventMapping? = nil) async -> (logic: AdClickAttributionLogic, startOfAttribution: Date) { @@ -232,21 +231,21 @@ final class AdClickAttributionLogicHelper { let mockRulesProvider = await MockAttributionRulesProvider() let mockDetection = AdClickAttributionDetection(feature: feature, tld: tld) - + mockRulesProvider.onRequestingAttribution = { vendor, completion in XCTAssertEqual(vendor, attributedVendorHost) completion(mockAttributedRules) } - + let logic = AdClickAttributionLogic(featureConfig: feature, rulesProvider: mockRulesProvider, tld: tld, eventReporting: eventReporting) - + logic.attributionDetection(mockDetection, didDetectVendor: attributedVendorHost) - + logic.onDidFinishNavigation(host: "sub.\(attributedVendorHost)") - + let startOfAttribution: Date! if case AdClickAttributionLogic.State.activeAttribution(_, let session, let rules) = logic.state { XCTAssertEqual(rules.identifier, mockAttributedRules?.identifier) @@ -255,40 +254,40 @@ final class AdClickAttributionLogicHelper { XCTFail("Attribution should be present") startOfAttribution = Date() } - + return (logic, startOfAttribution) } } final class AdClickAttributionLogicTimeoutTests: XCTestCase { - + func testWhenAttributionIsActiveThenTotalTimeoutApplies() async { let (logic, startOfAttribution) = await AdClickAttributionLogicHelper.prepareLogic(attributedVendorHost: "example.com") let feature = AdClickAttributionLogicHelper.feature - + logic.onProvisionalNavigation(completion: {}, currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution = logic.state { } else { XCTFail("Attribution should be present") } - + logic.onProvisionalNavigation(completion: {}, currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration)) if case AdClickAttributionLogic.State.noAttribution = logic.state { } else { XCTFail("Attribution should be forgotten") } } - + func testWhenAttributionIsInactiveThenNavigationalTimeoutApplies() async { let (logic, startOfAttribution) = await AdClickAttributionLogicHelper.prepareLogic(attributedVendorHost: "example.com") let feature = AdClickAttributionLogicHelper.feature - + logic.onDidFinishNavigation(host: "example.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution = logic.state { } else { XCTFail("Attribution should be present") } - + logic.onProvisionalNavigation(completion: {}, currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution(_, let session, _) = logic.state { @@ -296,7 +295,7 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + var leftAttributionContextAt: Date! = nil logic.onDidFinishNavigation(host: "other.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) @@ -306,30 +305,30 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + logic.onProvisionalNavigation(completion: {}, currentTime: leftAttributionContextAt.addingTimeInterval(feature.navigationExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution = logic.state { } else { XCTFail("Attribution should be present") } - + logic.onProvisionalNavigation(completion: {}, currentTime: leftAttributionContextAt.addingTimeInterval(feature.navigationExpiration)) if case AdClickAttributionLogic.State.noAttribution = logic.state { } else { XCTFail("Attribution should be forgotten") } } - + func testWhenAttributionIsReappliedThenTotalTimeoutApplies() async { let (logic, startOfAttribution) = await AdClickAttributionLogicHelper.prepareLogic(attributedVendorHost: "example.com") let feature = AdClickAttributionLogicHelper.feature - + logic.onDidFinishNavigation(host: "example.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution = logic.state { } else { XCTFail("Attribution should be present") } - + logic.onDidFinishNavigation(host: "other.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution(_, let session, _) = logic.state { @@ -337,7 +336,7 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + logic.onProvisionalNavigation(completion: {}, currentTime: startOfAttribution.addingTimeInterval(feature.navigationExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution(_, let session, _) = logic.state { @@ -345,7 +344,7 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + logic.onDidFinishNavigation(host: "example.com", currentTime: startOfAttribution.addingTimeInterval(feature.navigationExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution(_, let session, _) = logic.state { @@ -353,13 +352,13 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + logic.onProvisionalNavigation(completion: {}, currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution = logic.state { } else { XCTFail("Attribution should be present") } - + logic.onDidFinishNavigation(host: "example.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution = logic.state { } else { @@ -372,17 +371,17 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { XCTFail("Attribution should be forgotten") } } - + func testWhenAttributionIsReappliedThenNavigationalTimeoutResetsForNextInactiveState() async { let (logic, startOfAttribution) = await AdClickAttributionLogicHelper.prepareLogic(attributedVendorHost: "example.com") let feature = AdClickAttributionLogicHelper.feature - + logic.onDidFinishNavigation(host: "example.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution = logic.state { } else { XCTFail("Attribution should be present") } - + var lastTimeOfLeavingAttributionSite: Date? logic.onDidFinishNavigation(host: "other.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) @@ -392,7 +391,7 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + logic.onDidFinishNavigation(host: "example.com", currentTime: startOfAttribution.addingTimeInterval(feature.navigationExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution(_, let session, _) = logic.state { @@ -400,7 +399,7 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + logic.onDidFinishNavigation(host: "other.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution(_, let session, _) = logic.state { @@ -410,7 +409,7 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + logic.onProvisionalNavigation(completion: {}, currentTime: lastTimeOfLeavingAttributionSite!.addingTimeInterval(feature.navigationExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution(_, let session, _) = logic.state { @@ -418,7 +417,7 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } else { XCTFail("Attribution should be present") } - + logic.onDidFinishNavigation(host: "something.com", currentTime: lastTimeOfLeavingAttributionSite!.addingTimeInterval(feature.navigationExpiration - 1)) if case AdClickAttributionLogic.State.activeAttribution(_, let session, _) = logic.state { @@ -437,7 +436,7 @@ final class AdClickAttributionLogicTimeoutTests: XCTestCase { } final class AdClickAttributionLogicStateInheritingTests: XCTestCase { - + static let tld = TLD() let feature = MockAttributing() @@ -445,94 +444,94 @@ final class AdClickAttributionLogicStateInheritingTests: XCTestCase { let (logic, startOfAttribution) = await AdClickAttributionLogicHelper.prepareLogic(attributedVendorHost: "example.com") let feature = AdClickAttributionLogicHelper.feature let rules = await ContentBlockingRulesHelper().makeFakeRules(name: "attributed")! - + logic.onDidFinishNavigation(host: "example.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) - + if case AdClickAttributionLogic.State.activeAttribution = logic.state { } else { XCTFail("Attribution should be present") } - + let inheritedSession = AdClickAttributionLogic.SessionInfo(start: startOfAttribution.addingTimeInterval(-1)) logic.applyInheritedAttribution(state: .activeAttribution(vendor: "example.com", session: inheritedSession, rules: rules)) - + logic.onDidFinishNavigation(host: "example.com", currentTime: startOfAttribution.addingTimeInterval(feature.totalExpiration - 1)) - + if case AdClickAttributionLogic.State.noAttribution = logic.state { } else { XCTFail("Attribution should be forgotten") } } - + func testWhenInactiveAttributionIsInheritedThenItIsIgnored() async { let mockRulesProvider = await MockAttributionRulesProvider() let rules = await ContentBlockingRulesHelper().makeFakeRules(name: "attributed")! - + let logic = AdClickAttributionLogic(featureConfig: feature, rulesProvider: mockRulesProvider, tld: Self.tld) - + if case AdClickAttributionLogic.State.noAttribution = logic.state { } else { XCTFail("Attribution should be present") } - + let inactiveSession = AdClickAttributionLogic.SessionInfo(start: Date(), leftContextAt: Date()) logic.applyInheritedAttribution(state: .activeAttribution(vendor: "example.com", session: inactiveSession, rules: rules)) - + if case AdClickAttributionLogic.State.noAttribution = logic.state { } else { XCTFail("Attribution should be forgotten") } } - + } final class AdClickAttributionLogicConfigUpdateTests: XCTestCase { - + static let tld = TLD() - + let feature = MockAttributing() let mockDelegate = MockAdClickAttributionLogicDelegate() - + func testWhenTDSUpdatesThenAttributedRulesAreRefreshed() async { let mockAttributedRules = await ContentBlockingRulesHelper().makeFakeRules(name: "attributed") let mockDetection = AdClickAttributionDetection(feature: feature, tld: Self.tld) - + let mockRulesProvider = await MockAttributionRulesProvider() - + let logic = AdClickAttributionLogic(featureConfig: feature, rulesProvider: mockRulesProvider, tld: Self.tld) - + logic.delegate = mockDelegate - + mockRulesProvider.onRequestingAttribution = { vendor, completion in XCTAssertEqual(vendor, "example.com") completion(mockAttributedRules) } - + let rulesApplied = expectation(description: "Rules Applied") mockDelegate.onRequestingRuleApplication = { rules in XCTAssertNotNil(rules?.name) XCTAssertEqual(rules?.name, mockAttributedRules?.name) rulesApplied.fulfill() } - + logic.attributionDetection(mockDetection, didDetectVendor: "example.com") await fulfillment(of: [rulesApplied], timeout: 0.1) - + // - Prepare callbacks for update let updatedAttributedRules = await ContentBlockingRulesHelper().makeFakeRules(name: "attributed_updated") mockRulesProvider.onRequestingAttribution = { vendor, completion in XCTAssertEqual(vendor, "example.com") completion(updatedAttributedRules) } - + let rulesUpdated = expectation(description: "Rules Updated") mockDelegate.onRequestingRuleApplication = { rules in XCTAssertNotNil(rules?.name) @@ -540,7 +539,7 @@ final class AdClickAttributionLogicConfigUpdateTests: XCTestCase { rulesUpdated.fulfill() } // - - + let updatedTDSRules = await ContentBlockingRulesHelper().makeFakeRules(name: "newTDS", tdsEtag: UUID().uuidString) XCTAssertNotNil(updatedTDSRules) diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionPixelTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionPixelTests.swift index 415cf30f0..01e26d73e 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionPixelTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionPixelTests.swift @@ -1,6 +1,5 @@ // -// AdClickAttributionDetectionTests.swift -// DuckDuckGo +// AdClickAttributionPixelTests.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -23,39 +22,39 @@ import ContentBlocking import Common final class AdClickAttributionPixelTests: XCTestCase { - + static let tld = TLD() - + static let noEventExpectedHandler: (AdClickAttributionEvents, [String: String]?) -> Void = { event, _ in XCTFail("Unexpected event: \(event)")} - + static let domainParameterName = "ad_domain_param.com" static let linkUrlWithParameter = URL(string: "https://example.com/test.html?\(domainParameterName)=test.com")! static let linkUrlWithoutParameter = URL(string: "https://example.com/test.html")! - + static let matchedVendorURL = URL(string: "https://test.com/site")! static let mismatchedVendorURL = URL(string: "https://other.com/site")! - + var currentEventHandler: (AdClickAttributionEvents, [String: String]?) -> Void = { _, _ in } - + lazy var mockEventMapping = EventMapping { event, _, params, _ in self.currentEventHandler(event, params) } - + func testWhenSERPAndHeuristicsMatchThenThisMatchIsSent() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return Self.domainParameterName }) feature.isDomainDetectionEnabled = true feature.isHeuristicDetectionEnabled = true - + currentEventHandler = Self.noEventExpectedHandler - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld, eventReporting: mockEventMapping) - + detection.onStartNavigation(url: Self.linkUrlWithParameter) - + let expectation = expectation(description: "Event fired") currentEventHandler = { event, params in expectation.fulfill() @@ -64,25 +63,25 @@ final class AdClickAttributionPixelTests: XCTestCase { XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.domainDetectionEnabled], "1") XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.heuristicDetectionEnabled], "1") } - + detection.on2XXResponse(url: Self.matchedVendorURL) wait(for: [expectation], timeout: 1) } - + func testWhenSERPAndHeuristicsDoNotMatchThenThisMismatchIsSent() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return Self.domainParameterName }) feature.isDomainDetectionEnabled = true feature.isHeuristicDetectionEnabled = true - + currentEventHandler = Self.noEventExpectedHandler - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld, eventReporting: mockEventMapping) - + detection.onStartNavigation(url: Self.linkUrlWithParameter) - + let expectation = expectation(description: "Event fired") currentEventHandler = { event, params in expectation.fulfill() @@ -91,21 +90,21 @@ final class AdClickAttributionPixelTests: XCTestCase { XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.domainDetectionEnabled], "1") XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.heuristicDetectionEnabled], "1") } - + detection.on2XXResponse(url: Self.mismatchedVendorURL) wait(for: [expectation], timeout: 1) } - + func testWhenHeuristicsAreDisabledAndSerpIsPresentThenSerpIsUsed() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return Self.domainParameterName }) feature.isDomainDetectionEnabled = true feature.isHeuristicDetectionEnabled = false - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld, eventReporting: mockEventMapping) - + let expectation = expectation(description: "Event fired") currentEventHandler = { event, params in expectation.fulfill() @@ -114,24 +113,24 @@ final class AdClickAttributionPixelTests: XCTestCase { XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.domainDetectionEnabled], "1") XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.heuristicDetectionEnabled], "0") } - + detection.onStartNavigation(url: Self.linkUrlWithParameter) wait(for: [expectation], timeout: 1) - + currentEventHandler = Self.noEventExpectedHandler detection.on2XXResponse(url: Self.matchedVendorURL) } - + func testWhenHeuristicsAreDisabledAndSerpIsMissingThenNoneIsSent() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return Self.domainParameterName }) feature.isDomainDetectionEnabled = true feature.isHeuristicDetectionEnabled = false - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld, eventReporting: mockEventMapping) - + let expectation = expectation(description: "Event fired") currentEventHandler = { event, params in expectation.fulfill() @@ -140,24 +139,24 @@ final class AdClickAttributionPixelTests: XCTestCase { XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.domainDetectionEnabled], "1") XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.heuristicDetectionEnabled], "0") } - + detection.onStartNavigation(url: Self.linkUrlWithoutParameter) wait(for: [expectation], timeout: 1) - + currentEventHandler = Self.noEventExpectedHandler detection.on2XXResponse(url: Self.matchedVendorURL) } - + func testWhenHeuristicsAndSerpAreDisabledThenNoneIsSent() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return Self.domainParameterName }) feature.isDomainDetectionEnabled = false feature.isHeuristicDetectionEnabled = false - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld, eventReporting: mockEventMapping) - + let expectation = expectation(description: "Event fired") currentEventHandler = { event, params in expectation.fulfill() @@ -166,29 +165,29 @@ final class AdClickAttributionPixelTests: XCTestCase { XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.domainDetectionEnabled], "0") XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.heuristicDetectionEnabled], "0") } - + detection.onStartNavigation(url: Self.linkUrlWithParameter) - + wait(for: [expectation], timeout: 1) - + currentEventHandler = Self.noEventExpectedHandler detection.on2XXResponse(url: Self.matchedVendorURL) } - + func testWhenSerpIsDisabledThenHeuristicsAreUsed() { - + let feature = MockAttributing(onParameterNameQuery: { _ in return Self.domainParameterName }) feature.isDomainDetectionEnabled = false feature.isHeuristicDetectionEnabled = true - + currentEventHandler = Self.noEventExpectedHandler - + let detection = AdClickAttributionDetection(feature: feature, tld: Self.tld, eventReporting: mockEventMapping) - + detection.onStartNavigation(url: Self.linkUrlWithParameter) - + let expectation = expectation(description: "Event fired") currentEventHandler = { event, params in expectation.fulfill() @@ -197,26 +196,26 @@ final class AdClickAttributionPixelTests: XCTestCase { XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.domainDetectionEnabled], "0") XCTAssertEqual(params?[AdClickAttributionEvents.Parameters.heuristicDetectionEnabled], "1") } - + detection.on2XXResponse(url: Self.matchedVendorURL) wait(for: [expectation], timeout: 1) } - + func testWhenAttributionIsInactiveThenNoActivityPixelIsSent() async { currentEventHandler = Self.noEventExpectedHandler - + let feature = MockAttributing() feature.onFormatMatching = { _ in return false } let mockRulesProvider = await MockAttributionRulesProvider() - + let logic = AdClickAttributionLogic(featureConfig: feature, rulesProvider: mockRulesProvider, tld: Self.tld, eventReporting: mockEventMapping) - + logic.onProvisionalNavigation {} logic.onDidFinishNavigation(host: "test.com") - + logic.onRequestDetected(request: DetectedRequest(url: "example.com", eTLDplus1: "example.com", knownTracker: nil, @@ -224,7 +223,7 @@ final class AdClickAttributionPixelTests: XCTestCase { state: .allowed(reason: .adClickAttribution), pageUrl: "test.com")) } - + func testWhenAttributionIsActiveThenActivityPixelIsSentOnce() async { let expectation = expectation(description: "Event fired") expectation.expectedFulfillmentCount = 1 @@ -232,35 +231,35 @@ final class AdClickAttributionPixelTests: XCTestCase { expectation.fulfill() XCTAssertEqual(event, AdClickAttributionEvents.adAttributionActive) } - + let feature = MockAttributing() let mockRulesProvider = await MockAttributionRulesProvider() let mockDetection = AdClickAttributionDetection(feature: feature, tld: Self.tld) - + let logic = AdClickAttributionLogic(featureConfig: feature, rulesProvider: mockRulesProvider, tld: Self.tld, eventReporting: mockEventMapping) - + logic.attributionDetection(mockDetection, didDetectVendor: "vendor.com") logic.onDidFinishNavigation(host: "https://vendor.com") - + logic.onRequestDetected(request: DetectedRequest(url: "example.com", eTLDplus1: "example.com", knownTracker: nil, entity: nil, state: .allowed(reason: .adClickAttribution), pageUrl: "test.com")) - + logic.onRequestDetected(request: DetectedRequest(url: "example.com", eTLDplus1: "example.com", knownTracker: nil, entity: nil, state: .allowed(reason: .adClickAttribution), pageUrl: "test.com")) - + await fulfillment(of: [expectation], timeout: 1) } - + } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesMutatorTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesMutatorTests.swift index 0c56f4fc8..441d08cc6 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesMutatorTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesMutatorTests.swift @@ -1,6 +1,5 @@ // // AdClickAttributionRulesMutatorTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,26 +21,26 @@ import XCTest @testable import TrackerRadarKit final class MockAttributionConfig: AdClickAttributing { - + func isMatchingAttributionFormat(_ url: URL) -> Bool { return true } - + func attributionDomainParameterName(for: URL) -> String? { return nil } - + var isEnabled = true var allowlist = [AdClickAttributionFeature.AllowlistEntry]() var navigationExpiration: Double = 0 var totalExpiration: Double = 0 var isHeuristicDetectionEnabled: Bool = true var isDomainDetectionEnabled: Bool = true - + } final class AdClickAttributionRulesMutatorTests: XCTestCase { - + let exampleTDS = """ { "trackers": { @@ -92,109 +91,109 @@ final class AdClickAttributionRulesMutatorTests: XCTestCase { "cnames": {} } """.data(using: .utf8)! - + func isEqualAsJson(l: T?, r: T?) throws -> Bool { guard let l = l, let r = r else { XCTFail("Could not encode objects") return false } - + let lData = try JSONEncoder().encode(l) let rData = try JSONEncoder().encode(r) - + return String(data: lData, encoding: .utf8) == String(data: rData, encoding: .utf8) } - + func testWhenEntityIsOnAllowlistThenRuleIsApplied() throws { let trackerData = try JSONDecoder().decode(TrackerData.self, from: exampleTDS) - + let mockConfig = MockAttributionConfig() mockConfig.allowlist.append(AdClickAttributionFeature.AllowlistEntry(entity: "example.com", host: "test.com")) - + let mutator = AdClickAttributionRulesMutator(trackerData: trackerData, config: mockConfig) let attributedRules = mutator.addException(vendorDomain: "vendor.com") - + XCTAssertNotNil(attributedRules.trackers["example.com"]) XCTAssertFalse(try isEqualAsJson(l: attributedRules.trackers["example.com"], r: trackerData.trackers["example.com"])) - + XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.count, 1) XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.first?.rule, "test\\.com(:[0-9]+)?/.*") XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.first?.exceptions?.domains, ["vendor.com"]) - + XCTAssert(try isEqualAsJson(l: attributedRules.trackers["examplerules.com"], r: trackerData.trackers["examplerules.com"])) - + XCTAssertEqual(trackerData.domains, attributedRules.domains) XCTAssertEqual(trackerData.entities, attributedRules.entities) XCTAssertEqual(trackerData.cnames, attributedRules.cnames) } - + func testWhenEntityHasMultipleEntriesOnAllowlistThenAllRulesAreApplied() throws { let trackerData = try JSONDecoder().decode(TrackerData.self, from: exampleTDS) - + let mockConfig = MockAttributionConfig() mockConfig.allowlist.append(AdClickAttributionFeature.AllowlistEntry(entity: "example.com", host: "test.com")) mockConfig.allowlist.append(AdClickAttributionFeature.AllowlistEntry(entity: "example.com", host: "test.org")) - + let mutator = AdClickAttributionRulesMutator(trackerData: trackerData, config: mockConfig) let attributedRules = mutator.addException(vendorDomain: "vendor.com") - + XCTAssertNotNil(attributedRules.trackers["example.com"]) XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.count, 2) XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.first?.rule, "test\\.org(:[0-9]+)?/.*") XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.first?.exceptions?.domains, ["vendor.com"]) XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.last?.rule, "test\\.com(:[0-9]+)?/.*") XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.last?.exceptions?.domains, ["vendor.com"]) - + XCTAssertEqual(attributedRules.trackers["examplerules.com"], trackerData.trackers["examplerules.com"]) - + XCTAssertEqual(trackerData.domains, attributedRules.domains) XCTAssertEqual(trackerData.entities, attributedRules.entities) XCTAssertEqual(trackerData.cnames, attributedRules.cnames) } - + func testWhenEntityIsNotOnAllowlistThenNothingChanges() throws { let trackerData = try JSONDecoder().decode(TrackerData.self, from: exampleTDS) - + let mockConfig = MockAttributionConfig() mockConfig.allowlist.append(AdClickAttributionFeature.AllowlistEntry(entity: "other.com", host: "test.com")) - + let mutator = AdClickAttributionRulesMutator(trackerData: trackerData, config: mockConfig) let attributedRules = mutator.addException(vendorDomain: "vendor.com") - + XCTAssert(try isEqualAsJson(l: attributedRules.trackers["example.com"], r: trackerData.trackers["example.com"])) XCTAssert(try isEqualAsJson(l: attributedRules.trackers["examplerules.com"], r: trackerData.trackers["examplerules.com"])) - + XCTAssertEqual(trackerData.trackers, attributedRules.trackers) XCTAssertEqual(trackerData.domains, attributedRules.domains) XCTAssertEqual(trackerData.entities, attributedRules.entities) XCTAssertEqual(trackerData.cnames, attributedRules.cnames) } - + func testWhenEntityExistingRulesThenTheyAreMergedWithAdditonalOnesAndAttributionsAreFirst() throws { let trackerData = try JSONDecoder().decode(TrackerData.self, from: exampleTDS) - + let mockConfig = MockAttributionConfig() mockConfig.allowlist.append(AdClickAttributionFeature.AllowlistEntry(entity: "example.com", host: "test.com")) mockConfig.allowlist.append(AdClickAttributionFeature.AllowlistEntry(entity: "examplerules.com", host: "test.org")) - + let mutator = AdClickAttributionRulesMutator(trackerData: trackerData, config: mockConfig) let attributedRules = mutator.addException(vendorDomain: "vendor.com") - + XCTAssertFalse(try isEqualAsJson(l: attributedRules.trackers["example.com"], r: trackerData.trackers["example.com"])) XCTAssertFalse(try isEqualAsJson(l: attributedRules.trackers["examplerules.com"], r: trackerData.trackers["examplerules.com"])) - + XCTAssertNotNil(attributedRules.trackers["example.com"]) XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.count, 1) XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.first?.rule, "test\\.com(:[0-9]+)?/.*") XCTAssertEqual(attributedRules.trackers["example.com"]?.rules?.first?.exceptions?.domains, ["vendor.com"]) - + XCTAssertNotNil(attributedRules.trackers["examplerules.com"]) XCTAssertEqual(attributedRules.trackers["examplerules.com"]?.rules?.count, 2) XCTAssertEqual(attributedRules.trackers["examplerules.com"]?.rules?.first?.rule, "test\\.org(:[0-9]+)?/.*") XCTAssertEqual(attributedRules.trackers["examplerules.com"]?.rules?.first?.exceptions?.domains, ["vendor.com"]) XCTAssertEqual(attributedRules.trackers["examplerules.com"]?.rules?.last?.rule, "example.com/customrule/1.js") XCTAssertNil(attributedRules.trackers["examplerules.com"]?.rules?.last?.exceptions?.domains) - + XCTAssertEqual(trackerData.domains, attributedRules.domains) XCTAssertEqual(trackerData.entities, attributedRules.entities) XCTAssertEqual(trackerData.cnames, attributedRules.cnames) diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesProviderTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesProviderTests.swift index 8525a4ae9..117cd5318 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesProviderTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesProviderTests.swift @@ -1,6 +1,5 @@ // -// AdClickAttributionDetectionTests.swift -// DuckDuckGo +// AdClickAttributionRulesProviderTests.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,13 +21,13 @@ import BrowserServicesKit import os class MockCompiledRuleListSource: CompiledRuleListsSource { - + var currentRules: [ContentBlockerRulesManager.Rules] { [currentMainRules, currentAttributionRules].compactMap { $0 } } - + var currentMainRules: ContentBlockerRulesManager.Rules? - + var onCurrentRulesQueried: () -> Void = { } var _currentAttributionRules: ContentBlockerRulesManager.Rules? @@ -48,23 +47,23 @@ class AdClickAttributionRulesProviderTests: XCTestCase { let feature = MockAttributing() let compiledRulesSource = MockCompiledRuleListSource() let exceptionsSource = MockContentBlockerRulesExceptionsSource() - + var fakeNewRules: ContentBlockerRulesManager.Rules! - + var provider: AdClickAttributionRulesProvider! override func setUp() async throws { try? await super.setUp() - + feature.allowlist = [AdClickAttributionFeature.AllowlistEntry(entity: "tracker.com", host: "sub.test.com")] - + compiledRulesSource.currentMainRules = await ContentBlockingRulesHelper().makeFakeRules(name: DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName, tdsEtag: "tdsEtag", tempListId: "tempEtag", allowListId: nil, unprotectedSitesHash: nil) - + let attributionName = AdClickAttributionRulesSplitter.blockingAttributionRuleListName(forListNamed: compiledRulesSource.currentMainRules!.name) compiledRulesSource.currentAttributionRules = await ContentBlockingRulesHelper().makeFakeRules(name: attributionName, tdsEtag: "tdsEtag", @@ -73,7 +72,7 @@ class AdClickAttributionRulesProviderTests: XCTestCase { unprotectedSitesHash: nil) XCTAssertNotNil(compiledRulesSource.currentMainRules) XCTAssertNotNil(compiledRulesSource.currentAttributionRules) - + fakeNewRules = await ContentBlockingRulesHelper().makeFakeRules(name: compiledRulesSource.currentAttributionRules!.name, tdsEtag: "updatedEtag", tempListId: "updatedEtag", @@ -85,15 +84,15 @@ class AdClickAttributionRulesProviderTests: XCTestCase { exceptionsSource: exceptionsSource, log: log) } - + func testWhenAttributionIsRequestedThenRulesArePrepared() { - + let rulesCompiled = expectation(description: "Rules should be compiled") provider.requestAttribution(forVendor: "vendor.com") { rules in rulesCompiled.fulfill() XCTAssertNotNil(rules) XCTAssertEqual(rules?.name, AdClickAttributionRulesProvider.Constants.attributedTempRuleListName) - + let tracker = rules?.trackerData.trackers["tracker.com"] XCTAssertNotNil(tracker) let rule = tracker?.rules?.first @@ -102,21 +101,21 @@ class AdClickAttributionRulesProviderTests: XCTestCase { XCTAssertEqual(rule?.action, .block) XCTAssertEqual(rule?.exceptions?.domains?.first, "vendor.com") } - + wait(for: [rulesCompiled], timeout: 5) } - + func testWhenAttributionIsRequestedMultipleTimesThenRulesArePreparedOnce() { - + let currentRulesQueried = expectation(description: "Current Rules should be queried") currentRulesQueried.expectedFulfillmentCount = 4 // 3 for set up, 1 for compilation compiledRulesSource.onCurrentRulesQueried = { currentRulesQueried.fulfill() } - + let rulesCompiled = expectation(description: "Rules should be compiled") rulesCompiled.expectedFulfillmentCount = 3 - + var compiledRules: [ContentBlockerRulesManager.Rules] = [] var identifiers: Set = [] provider.requestAttribution(forVendor: "vendor.com") { rules in @@ -134,26 +133,26 @@ class AdClickAttributionRulesProviderTests: XCTestCase { compiledRules.append(rules!) identifiers.insert(rules!.identifier.stringValue) } - + wait(for: [rulesCompiled, currentRulesQueried], timeout: 5) - + XCTAssertEqual(compiledRules.count, 3) XCTAssertEqual(identifiers.count, 1) XCTAssert(compiledRules[0].rulesList === compiledRules[1].rulesList) XCTAssert(compiledRules[0].rulesList === compiledRules[2].rulesList) } - + func testWhenAttributionIsRequestedForMultipleVendorsThenAllRulesArePrepared() { - + let currentRulesQueried = expectation(description: "Current Rules should be queried") currentRulesQueried.expectedFulfillmentCount = 5 // 3 for set up, 2 for compilation compiledRulesSource.onCurrentRulesQueried = { currentRulesQueried.fulfill() } - + let rulesCompiled = expectation(description: "Rules should be compiled") rulesCompiled.expectedFulfillmentCount = 3 - + var compiledRules: [ContentBlockerRulesManager.Rules] = [] var identifiers: Set = [] provider.requestAttribution(forVendor: "vendor.com") { rules in // #1 @@ -171,25 +170,25 @@ class AdClickAttributionRulesProviderTests: XCTestCase { compiledRules.append(rules!) identifiers.insert(rules!.identifier.stringValue) } - + wait(for: [rulesCompiled, currentRulesQueried], timeout: 10) - + XCTAssertEqual(compiledRules.count, 3) XCTAssert(compiledRules[0].rulesList === compiledRules[1].rulesList) // #1 and #3 are returned first XCTAssert(compiledRules[0].rulesList !== compiledRules[2].rulesList) // #2 is compiled afterwards } - + func testWhenAttributionIsRequestedForMultipleVendorsAndRulesChangeThenAllRulesAreCompiled() { - + let currentRulesQueried = expectation(description: "Current Rules should be queried") currentRulesQueried.expectedFulfillmentCount = 6 // 3 for set up, 3 for compilation compiledRulesSource.onCurrentRulesQueried = { currentRulesQueried.fulfill() } - + let rulesCompiled = expectation(description: "Rules should be compiled") rulesCompiled.expectedFulfillmentCount = 3 - + var compiledRules: [ContentBlockerRulesManager.Rules] = [] var identifiers: Set = [] provider.requestAttribution(forVendor: "vendor.com") { rules in // #1 @@ -197,10 +196,10 @@ class AdClickAttributionRulesProviderTests: XCTestCase { compiledRules.append(rules!) identifiers.insert(rules!.identifier.stringValue) } - + // Simulate rule list change self.compiledRulesSource.currentAttributionRules = self.fakeNewRules - + provider.requestAttribution(forVendor: "other.com") { rules in // #2 rulesCompiled.fulfill() compiledRules.append(rules!) @@ -211,12 +210,12 @@ class AdClickAttributionRulesProviderTests: XCTestCase { compiledRules.append(rules!) identifiers.insert(rules!.identifier.stringValue) } - + wait(for: [rulesCompiled, currentRulesQueried], timeout: 10) - + XCTAssertEqual(compiledRules.count, 3) XCTAssert(compiledRules[0].rulesList !== compiledRules[1].rulesList) XCTAssert(compiledRules[0].rulesList !== compiledRules[2].rulesList) } - + } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesSplitterTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesSplitterTests.swift index a7ae72a27..48687856d 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesSplitterTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionRulesSplitterTests.swift @@ -1,6 +1,5 @@ // // AdClickAttributionRulesSplitterTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -22,7 +21,7 @@ import XCTest @testable import TrackerRadarKit final class AdClickAttributionRulesSplitterTests: XCTestCase { - + func testShouldNotSplitIfThereAreNoTrackerNames() { // given let trackerData = TrackerData(trackers: [:], entities: [:], domains: [:], cnames: nil) @@ -71,13 +70,13 @@ final class AdClickAttributionRulesSplitterTests: XCTestCase { // original list XCTAssertEqual(result!.0.name, rulesList.name) - + let attributionNamePrefix = AdClickAttributionRulesSplitter.Constants.attributionRuleListNamePrefix let attributionEtagPrefix = AdClickAttributionRulesSplitter.Constants.attributionRuleListETagPrefix - + XCTAssertEqual(result!.0.trackerData!.etag, attributionEtagPrefix + rulesList.trackerData!.etag) XCTAssertEqual(result!.0.fallbackTrackerData.etag, attributionEtagPrefix + rulesList.fallbackTrackerData.etag) - + XCTAssertTrue(result!.0.trackerData!.tds.trackers.isEmpty) XCTAssertTrue(result!.0.fallbackTrackerData.tds.trackers.isEmpty) @@ -89,9 +88,9 @@ final class AdClickAttributionRulesSplitterTests: XCTestCase { XCTAssertEqual(result!.1.fallbackTrackerData.tds, rulesList.fallbackTrackerData.tds) } - + func testWhenSplittingManyTrackersThenDomainsRelatedToEntitiesArePreserved() { - + // given let allowlistedTrackerNames = ["trackerone.com"] let trackerData = TrackerData(trackers: ["trackerone.com": makeKnownTracker(withName: "trackerone.com", @@ -113,23 +112,23 @@ final class AdClickAttributionRulesSplitterTests: XCTestCase { // when let result = splitter.split() - + // attribution list - + guard let attributionTDS = result!.1.trackerData else { XCTFail("No attribution list found") return } - + let attributionEtagPrefix = AdClickAttributionRulesSplitter.Constants.attributionRuleListETagPrefix XCTAssertEqual(attributionTDS.etag, attributionEtagPrefix + rulesList.trackerData!.etag) - + XCTAssertEqual(attributionTDS.tds.trackers.count, 1) XCTAssertEqual(attributionTDS.tds.trackers.first?.key, "trackerone.com") XCTAssertEqual(attributionTDS.tds.entities.count, 1) XCTAssertEqual(attributionTDS.tds.entities.first?.key, "Tracker Owner") XCTAssertEqual(Set(attributionTDS.tds.domains.keys), Set(["example.com", "trackerone.com"])) - + } private func makeKnownTracker(withName name: String, ownerName: String) -> KnownTracker { @@ -145,5 +144,5 @@ final class AdClickAttributionRulesSplitterTests: XCTestCase { private func makeEntity(withName name: String, domains: [String]) -> Entity { Entity(displayName: name, domains: domains, prevalence: 5.0) } - + } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerReferenceTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerReferenceTests.swift index 9055a63ad..cd6020298 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerReferenceTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerReferenceTests.swift @@ -1,6 +1,5 @@ // // ContentBlockerReferenceTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -205,5 +204,5 @@ class ContentBlockerReferenceTests: XCTestCase { } } // swiftlint:enable function_body_length - + } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerInitialCompilationTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerInitialCompilationTests.swift index 5c574046a..f197328a1 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerInitialCompilationTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerInitialCompilationTests.swift @@ -1,6 +1,5 @@ // -// ContentBlockerRulesManagerTests.swift -// DuckDuckGo +// ContentBlockerRulesManagerInitialCompilationTests.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -33,18 +32,18 @@ final class CountedFulfillmentTestExpectation: XCTestExpectation { } final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { - + private static let fakeEmbeddedDataSet = ContentBlockerRulesManagerTests.makeDataSet(tds: ContentBlockerRulesManagerTests.validRules, etag: "\"\(UUID().uuidString)\"") private let rulesUpdateListener = RulesUpdateListener() - + func testSuccessfulCompilationStoresLastCompiledRules() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: ContentBlockerRulesManagerTests.makeDataSet(tds: ContentBlockerRulesManagerTests.validRules, etag: ContentBlockerRulesManagerTests.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() let mockLastCompiledRulesStore = MockLastCompiledRulesStore() - + let exp = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() @@ -54,23 +53,23 @@ final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { mockLastCompiledRulesStore.onRulesSet = { expStore.fulfill() } - + let cbrm = ContentBlockerRulesManager(rulesSource: mockRulesSource, exceptionsSource: mockExceptionsSource, lastCompiledRulesStore: mockLastCompiledRulesStore, updateListener: rulesUpdateListener) - + wait(for: [exp, expStore], timeout: 15.0) - + XCTAssertNotNil(mockLastCompiledRulesStore.rules) XCTAssertEqual(mockLastCompiledRulesStore.rules.first?.etag, mockRulesSource.trackerData?.etag) XCTAssertEqual(mockLastCompiledRulesStore.rules.first?.name, mockRulesSource.ruleListName) XCTAssertEqual(mockLastCompiledRulesStore.rules.first?.trackerData, mockRulesSource.trackerData?.tds) XCTAssertEqual(mockLastCompiledRulesStore.rules.first?.identifier, cbrm.currentRules.first?.identifier) } - + func testInitialCompilation_WhenNoChangesToTDS_ShouldNotFetchLastCompiled() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: ContentBlockerRulesManagerTests.makeDataSet(tds: ContentBlockerRulesManagerTests.validRules, etag: ContentBlockerRulesManagerTests.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) @@ -90,7 +89,7 @@ final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { mockLastCompiledRulesStore.onRulesGet = { XCTFail("Should use rules cached by WebKit") } - + // simulate the rules have been compiled in the past so the WKContentRuleListStore contains it _ = ContentBlockerRulesManager(rulesSource: mockRulesSource, exceptionsSource: mockExceptionsSource, @@ -110,7 +109,7 @@ final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { } wait(for: [exp], timeout: 15.0) - + func assertRules(_ rules: [ContentBlockerRulesManager.Rules]) { guard let rules = rules.first else { XCTFail("Couldn't get rules"); return } XCTAssertEqual(cachedRules.etag, rules.etag) @@ -183,10 +182,10 @@ final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { wait(for: [expCacheLookup, expNext], timeout: 15.0) } - + // swiftlint:disable:next function_body_length func testInitialCompilation_WhenThereAreChangesToTDS_ShouldBuildRulesUsingLastCompiledRulesAndScheduleRecompilationWithNewSource() { - + let oldEtag = ContentBlockerRulesManagerTests.makeEtag() let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: ContentBlockerRulesManagerTests.makeDataSet(tds: ContentBlockerRulesManagerTests.validRules, etag: oldEtag), @@ -211,9 +210,9 @@ final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { trackerData: mockRulesSource.trackerData!.tds, etag: mockRulesSource.trackerData!.etag, identifier: oldIdentifier) - + mockLastCompiledRulesStore.rules = [cachedRules] - + // simulate the rules have been compiled in the past so the WKContentRuleListStore contains it _ = ContentBlockerRulesManager(rulesSource: mockRulesSource, exceptionsSource: mockExceptionsSource, @@ -257,16 +256,16 @@ final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { wait(for: [expLastCompiledFetched, expRecompiled], timeout: 15.0) } - + struct MockLastCompiledRules: LastCompiledRules { - + var name: String var trackerData: TrackerData var etag: String var identifier: ContentBlockerRulesIdentifier - + } - + final class MockLastCompiledRulesStore: LastCompiledRulesStore { var onRulesGet: () -> Void = { } @@ -283,7 +282,7 @@ final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { _rules = newValue } } - + func update(with contentBlockerRules: [ContentBlockerRulesManager.Rules]) { rules = contentBlockerRules.map { rules in MockLastCompiledRules(name: rules.name, @@ -292,7 +291,7 @@ final class ContentBlockerRulesManagerInitialCompilationTests: XCTestCase { identifier: rules.identifier) } } - + } - + } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerMultipleRulesTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerMultipleRulesTests.swift index 6aeb8e22b..caa6b1199 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerMultipleRulesTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerMultipleRulesTests.swift @@ -1,6 +1,5 @@ // // ContentBlockerRulesManagerMultipleRulesTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -25,7 +24,7 @@ import Common // swiftlint:disable unused_closure_parameter class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTests { - + let firstRules = """ { "trackers": { @@ -72,7 +71,7 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe } } """ - + let secondRules = """ { "trackers": { @@ -119,10 +118,10 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe } } """ - + class MockContentBlockerRulesListsSource: ContentBlockerRulesListsSource { let contentBlockerRulesLists: [ContentBlockerRulesList] - + init(firstName: String, firstTD: TrackerDataManager.DataSet?, firstFallbackTD: TrackerDataManager.DataSet, secondName: String, secondTD: TrackerDataManager.DataSet?, secondFallbackTD: TrackerDataManager.DataSet) { contentBlockerRulesLists = [ContentBlockerRulesList(name: firstName, @@ -133,9 +132,9 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe fallbackTrackerData: secondFallbackTD)] } } - + private let rulesUpdateListener = RulesUpdateListener() - + let schemeHandler = TestSchemeHandler() let navigationDelegateMock = MockNavigationDelegate() @@ -145,22 +144,22 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe schemeHandler: TestSchemeHandler) -> WKWebView { XCTAssertFalse(currentRules.isEmpty) - + let configuration = WKWebViewConfiguration() configuration.setURLSchemeHandler(schemeHandler, forURLScheme: schemeHandler.scheme) - + let webView = WKWebView(frame: .init(origin: .zero, size: .init(width: 500, height: 1000)), configuration: configuration) webView.navigationDelegate = self.navigationDelegateMock - + for rule in currentRules { configuration.userContentController.add(rule.rulesList) } return webView } - + func testCompilationOfMultipleRulesListsWithSameETag() { - + let sharedETag = Self.makeEtag() let mockRulesSource = MockContentBlockerRulesListsSource(firstName: "first", firstTD: Self.makeDataSet(tds: firstRules, etag: sharedETag), @@ -181,9 +180,9 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe updateListener: rulesUpdateListener) wait(for: [exp], timeout: 15.0) - + XCTAssertFalse(cbrm.currentRules.isEmpty) - + for rules in cbrm.currentRules { if let source = mockRulesSource.contentBlockerRulesLists.first(where: { $0.name == rules.name }) { XCTAssertEqual(source.trackerData?.etag, rules.etag) @@ -191,12 +190,12 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe XCTFail("Missing rules") } } - + XCTAssertNotEqual(cbrm.currentRules[0].identifier.stringValue, cbrm.currentRules[1].identifier.stringValue) } - + func testBrokenTDSRecompilationAndFallback() { - + let invalidRulesETag = Self.makeEtag() let mockRulesSource = MockContentBlockerRulesListsSource(firstName: "first", firstTD: Self.makeDataSet(tds: Self.invalidRules, etag: invalidRulesETag), @@ -211,12 +210,12 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() } - + let errorExp = expectation(description: "No error reported") errorExp.expectedFulfillmentCount = 2 var brokenLists = Set() var errorComponents = Set() - let errorHandler = EventMapping.init { event, error, params, onComplete in + let errorHandler = EventMapping { event, error, params, onComplete in if case .contentBlockingCompilationFailed(let listName, let component) = event { brokenLists.insert(listName) errorComponents.insert(component) @@ -230,12 +229,12 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe errorReporting: errorHandler) wait(for: [exp, errorExp], timeout: 15.0) - + XCTAssertEqual(brokenLists, Set(["first", "second"])) XCTAssertEqual(errorComponents, Set([.tds])) - + XCTAssertFalse(cbrm.currentRules.isEmpty) - + for rules in cbrm.currentRules { if let source = mockRulesSource.contentBlockerRulesLists.first(where: { $0.name == rules.name }) { XCTAssertEqual(source.fallbackTrackerData.etag, rules.etag) @@ -244,9 +243,9 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe } } } - + func testCompilationOfMultipleRulesLists() { - + let mockRulesSource = MockContentBlockerRulesListsSource(firstName: "first", firstTD: Self.makeDataSet(tds: firstRules), firstFallbackTD: Self.makeDataSet(tds: firstRules), @@ -266,9 +265,9 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe updateListener: rulesUpdateListener) wait(for: [exp], timeout: 15.0) - + XCTAssertFalse(cbrm.currentRules.isEmpty) - + for rules in cbrm.currentRules { if let source = mockRulesSource.contentBlockerRulesLists.first(where: { $0.name == rules.name }) { XCTAssertEqual(source.trackerData?.etag, rules.etag) @@ -277,9 +276,9 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe } } } - + func testCompilationOfMultipleFallbackRulesLists() { - + let mockRulesSource = MockContentBlockerRulesListsSource(firstName: "first", firstTD: nil, firstFallbackTD: Self.makeDataSet(tds: firstRules), @@ -299,9 +298,9 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe updateListener: rulesUpdateListener) wait(for: [exp], timeout: 15.0) - + XCTAssertFalse(cbrm.currentRules.isEmpty) - + for rules in cbrm.currentRules { if let source = mockRulesSource.contentBlockerRulesLists.first(where: { $0.name == rules.name }) { XCTAssertEqual(source.fallbackTrackerData.etag, rules.etag) @@ -310,9 +309,9 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe } } } - + func testBrokenFallbackTDSFailure() { - + let mockRulesSource = MockContentBlockerRulesListsSource(firstName: "first", firstTD: Self.makeDataSet(tds: Self.invalidRules), firstFallbackTD: Self.makeDataSet(tds: Self.invalidRules), @@ -326,12 +325,12 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() } - + let errorExp = expectation(description: "No error reported") errorExp.expectedFulfillmentCount = 2 var brokenLists = Set() var errorComponents = Set() - let errorHandler = EventMapping.init { event, error, params, onComplete in + let errorHandler = EventMapping { event, error, params, onComplete in if case .contentBlockingCompilationFailed(let listName, let component) = event { brokenLists.insert(listName) errorComponents.insert(component) @@ -345,13 +344,13 @@ class ContentBlockerRulesManagerMultipleRulesTests: ContentBlockerRulesManagerTe errorReporting: errorHandler) wait(for: [exp, errorExp], timeout: 15.0) - + XCTAssertEqual(brokenLists, Set(["first"])) XCTAssertEqual(errorComponents, Set([.tds, .fallbackTds])) - + XCTAssertEqual(cbrm.currentRules.count, 1) XCTAssertEqual(cbrm.currentRules.first?.name, "second") } - + } // swiftlint:enable unused_closure_parameter diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerTests.swift index 6ada850f3..dc1e188ce 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesManagerTests.swift @@ -1,6 +1,5 @@ // // ContentBlockerRulesManagerTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -25,7 +24,7 @@ import Combine import Common class ContentBlockerRulesManagerTests: XCTestCase { - + static let validRules = """ { "trackers": { @@ -72,7 +71,7 @@ class ContentBlockerRulesManagerTests: XCTestCase { } } """ - + static let invalidRules = """ { "trackers": { @@ -139,35 +138,35 @@ class ContentBlockerRulesManagerTests: XCTestCase { } } """ - + let validTempSites = ["example.com"] let invalidTempSites = ["This is not valid.. ."] let validAllowList = [TrackerException(rule: "tracker.com/", matching: .all)] let invalidAllowList = [TrackerException(rule: "tracker.com/", matching: .domains(["broken site Ltd. . 😉.com"]))] - + static var fakeEmbeddedDataSet: TrackerDataManager.DataSet! - + override class func setUp() { super.setUp() - + fakeEmbeddedDataSet = makeDataSet(tds: validRules, etag: "\"\(UUID().uuidString)\"") } - + static func makeDataSet(tds: String) -> TrackerDataManager.DataSet { return makeDataSet(tds: tds, etag: makeEtag()) } - + static func makeDataSet(tds: String, etag: String) -> TrackerDataManager.DataSet { let data = tds.data(using: .utf8)! let decoded = try? JSONDecoder().decode(TrackerData.self, from: data) return (decoded!, etag) } - + static func makeEtag() -> String { return "\"\(UUID().uuidString)\"" } - + } final class RulesUpdateListener: ContentBlockerRulesUpdating { @@ -185,9 +184,9 @@ final class RulesUpdateListener: ContentBlockerRulesUpdating { class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { private let rulesUpdateListener = RulesUpdateListener() - + func test_ValidTDS_NoTempList_NoAllowList_NoUnprotectedSites() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: (Self.fakeEmbeddedDataSet.tds, Self.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() @@ -198,11 +197,11 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() } - + let errorExp = expectation(description: "No error reported") errorExp.isInverted = true let compilationTimeExp = expectation(description: "Compilation Time reported") - let errorHandler = EventMapping.init { event, _, params, _ in + let errorHandler = EventMapping { event, _, params, _ in if case .contentBlockingCompilationFailed(let listName, let component) = event { XCTAssertEqual(listName, DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName) switch component { @@ -211,7 +210,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { default: XCTFail("Unexpected component: \(component)") } - + } else if case .contentBlockingCompilationTime = event { XCTAssertNotNil(params?["compilationTime"]) compilationTimeExp.fulfill() @@ -226,7 +225,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { errorReporting: errorHandler) wait(for: [exp, errorExp, compilationTimeExp], timeout: 15.0) - + XCTAssertNotNil(cbrm.currentRules) XCTAssertEqual(cbrm.currentRules.first?.etag, mockRulesSource.trackerData?.etag) @@ -237,22 +236,22 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { allowListId: nil, unprotectedSitesHash: nil)) } - + func test_InvalidTDS_NoTempList_NoAllowList_NoUnprotectedSites() { let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.invalidRules, etag: Self.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let exp = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() } - + let errorExp = expectation(description: "Error reported") - let errorHandler = EventMapping.init { event, _, params, _ in + let errorHandler = EventMapping { event, _, params, _ in if case .contentBlockingCompilationFailed(let listName, let component) = event { XCTAssertEqual(listName, DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName) switch component { @@ -261,7 +260,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { default: XCTFail("Unexpected component: \(component)") } - + } else if case .contentBlockingCompilationTime = event { XCTAssertNotNil(params?["compilationTime"]) } else { @@ -275,7 +274,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { errorReporting: errorHandler) wait(for: [exp, errorExp], timeout: 15.0) - + XCTAssertNotNil(cbrm.currentRules) XCTAssertEqual(cbrm.currentRules.first?.etag, mockRulesSource.embeddedTrackerData.etag) @@ -286,7 +285,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { allowListId: nil, unprotectedSitesHash: nil)) } - + func test_ValidTDS_ValidTempList_NoAllowList_NoUnprotectedSites() { let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.validRules, etag: Self.makeEtag()), @@ -294,9 +293,9 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() mockExceptionsSource.tempListId = Self.makeEtag() mockExceptionsSource.tempList = validTempSites - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let exp = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() @@ -307,7 +306,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { updateListener: rulesUpdateListener) wait(for: [exp], timeout: 15.0) - + XCTAssertNotNil(cbrm.currentRules) XCTAssertNotNil(cbrm.currentRules.first?.etag) XCTAssertEqual(cbrm.currentRules.first?.etag, mockRulesSource.trackerData?.etag) @@ -319,7 +318,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { allowListId: nil, unprotectedSitesHash: nil)) } - + func test_InvalidTDS_ValidTempList_NoAllowList_NoUnprotectedSites() { let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.invalidRules, etag: Self.makeEtag()), @@ -327,9 +326,9 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() mockExceptionsSource.tempListId = Self.makeEtag() mockExceptionsSource.tempList = validTempSites - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let exp = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() @@ -340,12 +339,12 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { updateListener: rulesUpdateListener) wait(for: [exp], timeout: 15.0) - + XCTAssertNotNil(cbrm.currentRules.first?.etag) - + XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tdsIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tdsIdentifier, mockRulesSource.trackerData?.etag) - + XCTAssertEqual(cbrm.currentRules.first?.etag, mockRulesSource.embeddedTrackerData.etag) XCTAssertEqual(cbrm.currentRules.first?.identifier, @@ -355,17 +354,17 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { allowListId: nil, unprotectedSitesHash: nil)) } - + func test_ValidTDS_InvalidTempList_NoAllowList_NoUnprotectedSites() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.validRules, etag: Self.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() mockExceptionsSource.tempListId = Self.makeEtag() mockExceptionsSource.tempList = invalidTempSites - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let exp = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() @@ -376,14 +375,14 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { updateListener: rulesUpdateListener) wait(for: [exp], timeout: 15.0) - + XCTAssertNotNil(cbrm.currentRules.first?.etag) XCTAssertEqual(cbrm.currentRules.first?.etag, mockRulesSource.embeddedTrackerData.etag) - + // TDS is also marked as invalid to simplify flow XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tdsIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tdsIdentifier, mockRulesSource.trackerData?.etag) - + XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tempListIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tempListIdentifier, mockExceptionsSource.tempListId) @@ -394,18 +393,18 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { allowListId: nil, unprotectedSitesHash: nil)) } - + func test_ValidTDS_ValidTempList_NoAllowList_ValidUnprotectedSites() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.validRules, etag: Self.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() mockExceptionsSource.tempListId = Self.makeEtag() mockExceptionsSource.tempList = validTempSites mockExceptionsSource.unprotectedSites = ["example.com"] - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let exp = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() @@ -416,14 +415,14 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { updateListener: rulesUpdateListener) wait(for: [exp], timeout: 15.0) - + XCTAssertNotNil(cbrm.currentRules.first?.etag) XCTAssertEqual(cbrm.currentRules.first?.etag, mockRulesSource.trackerData?.etag) - + XCTAssertNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tdsIdentifier) XCTAssertNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tempListIdentifier) XCTAssertNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.unprotectedSitesIdentifier) - + XCTAssertEqual(cbrm.currentRules.first?.identifier, ContentBlockerRulesIdentifier(name: DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName, tdsEtag: mockRulesSource.trackerData?.etag ?? "\"\"", @@ -433,7 +432,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { } func test_ValidTDS_ValidTempList_ValidAllowList_ValidUnprotectedSites() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.validRules, etag: Self.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() @@ -516,9 +515,9 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { allowListId: nil, unprotectedSitesHash: mockExceptionsSource.unprotectedSitesHash)) } - + func test_ValidTDS_ValidTempList_ValidAllowList_BrokenUnprotectedSites() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.validRules, etag: Self.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() @@ -527,18 +526,18 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { mockExceptionsSource.allowListId = Self.makeEtag() mockExceptionsSource.allowList = validAllowList mockExceptionsSource.unprotectedSites = ["broken site Ltd. . 😉.com"] - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let exp = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() } - + let errorExp = expectation(description: "Error reported") errorExp.expectedFulfillmentCount = 5 var errorEvents = [ContentBlockerDebugEvents.Component]() - let errorHandler = EventMapping.init { event, _, params, _ in + let errorHandler = EventMapping { event, _, params, _ in if case .contentBlockingCompilationFailed(let listName, let component) = event { XCTAssertEqual(listName, DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName) errorEvents.append(component) @@ -548,7 +547,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { default: XCTFail("Unexpected component: \(component)") } - + } else if case .contentBlockingCompilationTime = event { XCTAssertNotNil(params?["compilationTime"]) errorExp.fulfill() @@ -563,20 +562,20 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { errorReporting: errorHandler) wait(for: [exp, errorExp], timeout: 15.0) - + XCTAssertEqual(Set(errorEvents), Set([.tds, .tempUnprotected, .allowlist, .localUnprotected])) - + XCTAssertNotNil(cbrm.currentRules.first?.etag) XCTAssertEqual(cbrm.currentRules.first?.etag, mockRulesSource.embeddedTrackerData.etag) - + // TDS is also marked as invalid to simplify flow XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tdsIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tdsIdentifier, mockRulesSource.trackerData?.etag) - + XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tempListIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tempListIdentifier, mockExceptionsSource.tempListId) @@ -584,7 +583,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.allowListIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.allowListIdentifier, mockExceptionsSource.allowListId) - + XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.unprotectedSitesIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.unprotectedSitesIdentifier, mockExceptionsSource.unprotectedSitesHash) @@ -596,9 +595,9 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { allowListId: nil, unprotectedSitesHash: nil)) } - + func test_CurrentTDSEqualToFallbackTDS_ValidTempList_ValidAllowList_BrokenUnprotectedSites() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.fakeEmbeddedDataSet, embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() @@ -607,18 +606,18 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { mockExceptionsSource.allowListId = Self.makeEtag() mockExceptionsSource.allowList = validAllowList mockExceptionsSource.unprotectedSites = ["broken site Ltd. . 😉.com"] - + XCTAssertEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let exp = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in exp.fulfill() } - + let errorExp = expectation(description: "Error reported") errorExp.expectedFulfillmentCount = 4 var errorEvents = [ContentBlockerDebugEvents.Component]() - let errorHandler = EventMapping.init { event, _, params, _ in + let errorHandler = EventMapping { event, _, params, _ in if case .contentBlockingCompilationFailed(let listName, let component) = event { XCTAssertEqual(listName, DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName) errorEvents.append(component) @@ -628,7 +627,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { default: XCTFail("Unexpected component: \(component)") } - + } else if case .contentBlockingCompilationTime = event { XCTAssertNotNil(params?["compilationTime"]) errorExp.fulfill() @@ -643,16 +642,16 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { errorReporting: errorHandler) wait(for: [exp, errorExp], timeout: 15.0) - + XCTAssertEqual(Set(errorEvents), Set([.tempUnprotected, .allowlist, .localUnprotected])) - + XCTAssertNotNil(cbrm.currentRules.first?.etag) XCTAssertEqual(cbrm.currentRules.first?.etag, mockRulesSource.embeddedTrackerData.etag) - + XCTAssertNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tdsIdentifier) - + XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tempListIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.tempListIdentifier, mockExceptionsSource.tempListId) @@ -660,7 +659,7 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.allowListIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.allowListIdentifier, mockExceptionsSource.allowListId) - + XCTAssertNotNil(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.unprotectedSitesIdentifier) XCTAssertEqual(cbrm.sourceManagers[mockRulesSource.ruleListName]?.brokenSources?.unprotectedSitesIdentifier, mockExceptionsSource.unprotectedSitesHash) @@ -677,17 +676,17 @@ class ContentBlockerRulesManagerLoadingTests: ContentBlockerRulesManagerTests { class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { private let rulesUpdateListener = RulesUpdateListener() - + func test_InvalidTDS_BeingFixed() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.invalidRules, etag: Self.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() mockExceptionsSource.tempListId = Self.makeEtag() mockExceptionsSource.tempList = validTempSites - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let initialLoading = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in initialLoading.fulfill() @@ -696,7 +695,7 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { let cbrm = ContentBlockerRulesManager(rulesSource: mockRulesSource, exceptionsSource: mockExceptionsSource, updateListener: rulesUpdateListener) - + wait(for: [initialLoading], timeout: 15.0) XCTAssertEqual(cbrm.currentRules.first?.identifier, @@ -705,9 +704,9 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { tempListId: mockExceptionsSource.tempListId, allowListId: nil, unprotectedSitesHash: nil)) - + mockRulesSource.trackerData = Self.makeDataSet(tds: Self.validRules, etag: Self.makeEtag()) - + let identifier = cbrm.currentRules.first?.identifier let updating = expectation(description: "Rules Compiled") @@ -716,7 +715,7 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { } cbrm.scheduleCompilation() - + wait(for: [updating], timeout: 15.0) XCTAssertEqual(cbrm.currentRules.first?.identifier.stringValue, @@ -725,10 +724,10 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { tempListId: mockExceptionsSource.tempListId, allowListId: nil, unprotectedSitesHash: nil).stringValue) - + if let oldId = identifier, let newId = cbrm.currentRules.first?.identifier { let diff = oldId.compare(with: newId) - + XCTAssert(diff.contains(.tdsEtag)) XCTAssertFalse(diff.contains(.tempListId)) XCTAssertFalse(diff.contains(.unprotectedSites)) @@ -736,17 +735,17 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { XCTFail("Missing identifiers") } } - + func test_InvalidTempList_BeingFixed() { - + let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.validRules, etag: Self.makeEtag()), embeddedTrackerData: Self.fakeEmbeddedDataSet) let mockExceptionsSource = MockContentBlockerRulesExceptionsSource() mockExceptionsSource.tempListId = Self.makeEtag() mockExceptionsSource.tempList = invalidTempSites - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let initialLoading = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in initialLoading.fulfill() @@ -755,7 +754,7 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { let cbrm = ContentBlockerRulesManager(rulesSource: mockRulesSource, exceptionsSource: mockExceptionsSource, updateListener: rulesUpdateListener) - + wait(for: [initialLoading], timeout: 15.0) XCTAssertEqual(cbrm.currentRules.first?.identifier, @@ -764,19 +763,19 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { tempListId: nil, allowListId: nil, unprotectedSitesHash: nil)) - + mockExceptionsSource.tempListId = Self.makeEtag() mockExceptionsSource.tempList = validTempSites - + let identifier = cbrm.currentRules.first?.identifier let updating = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in updating.fulfill() } - + cbrm.scheduleCompilation() - + wait(for: [updating], timeout: 15.0) XCTAssertEqual(cbrm.currentRules.first?.identifier, @@ -785,10 +784,10 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { tempListId: mockExceptionsSource.tempListId, allowListId: nil, unprotectedSitesHash: nil)) - + if let oldId = identifier, let newId = cbrm.currentRules.first?.identifier { let diff = oldId.compare(with: newId) - + XCTAssert(diff.contains(.tdsEtag)) XCTAssert(diff.contains(.tempListId)) XCTAssertFalse(diff.contains(.unprotectedSites)) @@ -856,7 +855,7 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { XCTFail("Missing identifiers") } } - + func test_InvalidUnprotectedSites_BeingFixed() { let mockRulesSource = MockSimpleContentBlockerRulesListsSource(trackerData: Self.makeDataSet(tds: Self.validRules, etag: Self.makeEtag()), @@ -865,9 +864,9 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { mockExceptionsSource.tempListId = Self.makeEtag() mockExceptionsSource.tempList = validTempSites mockExceptionsSource.unprotectedSites = ["broken site Ltd. . 😉.com"] - + XCTAssertNotEqual(mockRulesSource.trackerData?.etag, mockRulesSource.embeddedTrackerData.etag) - + let initialLoading = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in initialLoading.fulfill() @@ -876,7 +875,7 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { let cbrm = ContentBlockerRulesManager(rulesSource: mockRulesSource, exceptionsSource: mockExceptionsSource, updateListener: rulesUpdateListener) - + wait(for: [initialLoading], timeout: 15.0) XCTAssertEqual(cbrm.currentRules.first?.identifier, @@ -885,18 +884,18 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { tempListId: nil, allowListId: nil, unprotectedSitesHash: nil)) - + mockExceptionsSource.unprotectedSites = ["example.com"] - + let identifier = cbrm.currentRules.first?.identifier let updating = expectation(description: "Rules Compiled") rulesUpdateListener.onRulesUpdated = { _ in updating.fulfill() } - + cbrm.scheduleCompilation() - + wait(for: [updating], timeout: 15.0) XCTAssertEqual(cbrm.currentRules.first?.identifier, @@ -905,10 +904,10 @@ class ContentBlockerRulesManagerUpdatingTests: ContentBlockerRulesManagerTests { tempListId: mockExceptionsSource.tempListId, allowListId: nil, unprotectedSitesHash: mockExceptionsSource.unprotectedSitesHash)) - + if let oldId = identifier, let newId = cbrm.currentRules.first?.identifier { let diff = oldId.compare(with: newId) - + XCTAssert(diff.contains(.tdsEtag)) XCTAssert(diff.contains(.tempListId)) XCTAssert(diff.contains(.unprotectedSites)) @@ -1098,7 +1097,7 @@ class ContentBlockerRulesManagerCleanupTests: ContentBlockerRulesManagerTests, C } class MockSimpleContentBlockerRulesListsSource: ContentBlockerRulesListsSource { - + var trackerData: TrackerDataManager.DataSet? { didSet { let trackerData = trackerData @@ -1117,20 +1116,20 @@ class MockSimpleContentBlockerRulesListsSource: ContentBlockerRulesListsSource { fallbackTrackerData: embeddedTrackerData)] } } - + var contentBlockerRulesLists: [ContentBlockerRulesList] - + var ruleListName = DefaultContentBlockerRulesListsSource.Constants.trackerDataSetRulesListName - + init(trackerData: TrackerDataManager.DataSet?, embeddedTrackerData: TrackerDataManager.DataSet) { self.trackerData = trackerData self.embeddedTrackerData = embeddedTrackerData - + contentBlockerRulesLists = [ContentBlockerRulesList(name: ruleListName, trackerData: trackerData, fallbackTrackerData: embeddedTrackerData)] } - + } class MockContentBlockerRulesExceptionsSource: ContentBlockerRulesExceptionsSource { @@ -1140,7 +1139,7 @@ class MockContentBlockerRulesExceptionsSource: ContentBlockerRulesExceptionsSour var allowListId: String = "" var allowList: [TrackerException] = [] var unprotectedSites: [String] = [] - + var unprotectedSitesHash: String { return ContentBlockerRulesIdentifier.hash(domains: unprotectedSites) } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesUserScriptsTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesUserScriptsTests.swift index 6157850f8..670e717ed 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesUserScriptsTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockerRulesUserScriptsTests.swift @@ -1,6 +1,5 @@ // // ContentBlockerRulesUserScriptsTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -76,7 +75,7 @@ class ContentBlockerRulesUserScriptsTests: XCTestCase { let userScriptDelegateMock = MockRulesUserScriptDelegate() let navigationDelegateMock = MockNavigationDelegate() let tld = TLD() - + var webView: WKWebView? let nonTrackerURL = URL(string: "test://nontracker.com/1.png")! @@ -92,7 +91,7 @@ class ContentBlockerRulesUserScriptsTests: XCTestCase { .init(type: .image, url: trackerURL), .init(type: .image, url: subTrackerURL)]) } - + private func setupWebViewForUserScripTests(trackerData: TrackerData, privacyConfig: PrivacyConfiguration, userScriptDelegate: ContentBlockerRulesUserScriptDelegate, @@ -212,11 +211,11 @@ class ContentBlockerRulesUserScriptsTests: XCTestCase { let blockedTrackers = Set(self.userScriptDelegateMock.detectedTrackers.filter { $0.isBlocked }.map { $0.domain }) XCTAssertTrue(blockedTrackers.isEmpty) - + // We don't report first party trackers let detectedTrackers = Set(self.userScriptDelegateMock.detectedTrackers.map { $0.domain }) XCTAssert(detectedTrackers.isEmpty) - + let expected3rdParty: Set = ["nontracker.com"] let detected3rdParty = Set(self.userScriptDelegateMock.detectedThirdPartyRequests.map { $0.domain }) XCTAssertEqual(detected3rdParty, expected3rdParty) @@ -229,7 +228,7 @@ class ContentBlockerRulesUserScriptsTests: XCTestCase { self.wait(for: [websiteLoaded], timeout: 30) } - + func testWhenThereIsFirstPartyRequestThenItIsNotBlocked() { let privacyConfig = WebKitTestHelper.preparePrivacyConfig(locallyUnprotected: [], @@ -247,7 +246,7 @@ class ContentBlockerRulesUserScriptsTests: XCTestCase { let expectedTrackers: Set = ["sub.tracker.com", "tracker.com"] let blockedTrackers = Set(self.userScriptDelegateMock.detectedTrackers.filter { $0.isBlocked }.map { $0.domain }) XCTAssertEqual(blockedTrackers, expectedTrackers) - + let detected3rdParty = Set(self.userScriptDelegateMock.detectedThirdPartyRequests.map { $0.domain }) XCTAssert(detected3rdParty.isEmpty) diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockingRulesHelper.swift b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockingRulesHelper.swift index a4553898b..10532b246 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockingRulesHelper.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/ContentBlockingRulesHelper.swift @@ -1,6 +1,5 @@ // // ContentBlockingRulesHelper.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -24,9 +23,9 @@ import WebKit @MainActor final class ContentBlockingRulesHelper { - + func makeFakeTDS() -> TrackerData { - + let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, owner: KnownTracker.Owner(name: "Tracker", displayName: "Tracker"), @@ -34,38 +33,38 @@ final class ContentBlockingRulesHelper { subdomains: nil, categories: nil, rules: nil) - + let entity = Entity(displayName: "Tracker", domains: ["tracker.com"], prevalence: 0.1) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker": entity], domains: ["tracker.com": "Tracker"], cnames: nil) - + return tds } - + func makeFakeRules(name: String) async -> ContentBlockerRulesManager.Rules? { return await makeFakeRules(name: name, tdsEtag: UUID().uuidString) } - + func makeFakeRules(name: String, tdsEtag: String, tempListId: String? = nil, allowListId: String? = nil, unprotectedSitesHash: String? = nil) async -> ContentBlockerRulesManager.Rules? { - + let identifier = ContentBlockerRulesIdentifier(name: name, tdsEtag: tdsEtag, tempListId: tempListId, allowListId: allowListId, unprotectedSitesHash: unprotectedSitesHash) let tds = makeFakeTDS() - + let builder = ContentBlockerRulesBuilder(trackerData: tds) let rules = builder.buildRules() - + let data: Data do { data = try JSONEncoder().encode(rules) @@ -74,12 +73,12 @@ final class ContentBlockingRulesHelper { } let ruleList = String(data: data, encoding: .utf8)! - + guard let compiledRules = try? await WKContentRuleListStore.default().compileContentRuleList(forIdentifier: identifier.stringValue, encodedContentRuleList: ruleList) else { return nil } - + return .init(name: name, rulesList: compiledRules, trackerData: tds, diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/DetectedRequestTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/DetectedRequestTests.swift index 6b57273f0..53a0cde7f 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/DetectedRequestTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/DetectedRequestTests.swift @@ -40,7 +40,7 @@ final class DetectedRequestTests: XCTestCase { XCTAssertEqual(tracker1.hashValue, tracker2.hashValue) XCTAssertEqual(tracker1, tracker2) } - + func testWhenTrackerRequestsHaveSameEntityButDifferentBlockedStatusThenHashIsNotEqualAndIsEqualsIsFalse() { let entity = Entity(displayName: "Entity", domains: nil, prevalence: nil) diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/DomainMatchingReportTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/DomainMatchingReportTests.swift index bb24ffa1a..ae9676ab4 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/DomainMatchingReportTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/DomainMatchingReportTests.swift @@ -1,6 +1,5 @@ // // DomainMatchingReportTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -32,25 +31,25 @@ class DomainMatchingReportTests: XCTestCase { let testJSON = data.fromJsonFile("Resources/privacy-reference-tests/tracker-radar-tests/TR-domain-matching/domain_matching_tests.json") let trackerData = try JSONDecoder().decode(TrackerData.self, from: trackerJSON) - + let refTests = try JSONDecoder().decode(RefTests.self, from: testJSON) let tests = refTests.domainTests.tests - + let resolver = TrackerResolver(tds: trackerData, unprotectedSites: [], tempList: [], tld: TLD()) for test in tests { - + let skip = test.exceptPlatforms?.contains("ios-browser") if skip == true { os_log("!!SKIPPING TEST: %s", test.name) continue } - + let tracker = resolver.trackerFromUrl(test.requestURL, pageUrlString: test.siteURL, resourceType: test.requestType, potentiallyBlocked: true) - + if test.expectAction == "block" { XCTAssertNotNil(tracker) XCTAssert(tracker?.isBlocked ?? false) diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/DomainMatchingTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/DomainMatchingTests.swift index 8dd6006a0..3c1a912b8 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/DomainMatchingTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/DomainMatchingTests.swift @@ -1,6 +1,5 @@ // // DomainMatchingTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -23,26 +22,26 @@ import Foundation import os.log struct RefTests: Decodable { - + struct Test: Decodable { - + let name: String let siteURL: String let requestURL: String let requestType: String let expectAction, expectExpression: String? let exceptPlatforms: [String]? - + } - + struct DomainTests: Decodable { - + let name: String let desc: String let tests: [Test] - + } - + let domainTests, surrogateTests: DomainTests } @@ -54,7 +53,7 @@ class DomainMatchingTests: XCTestCase { let testJSON = data.fromJsonFile("Resources/privacy-reference-tests/tracker-radar-tests/TR-domain-matching/domain_matching_tests.json") let trackerData = try JSONDecoder().decode(TrackerData.self, from: trackerJSON) - + let refTests = try JSONDecoder().decode(RefTests.self, from: testJSON) let tests = refTests.domainTests.tests @@ -88,7 +87,7 @@ extension Array where Element == ContentBlockerRule { for rule in self where rule.matches(resourceUrl: url, onPageWithUrl: topLevel, ofType: resourceType) { result = rule } - + return result } } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/MockWebsite.swift b/Tests/BrowserServicesKitTests/ContentBlocker/MockWebsite.swift index fff7ad939..f32d5b332 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/MockWebsite.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/MockWebsite.swift @@ -1,6 +1,5 @@ // // MockWebsite.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/SurrogatesReferenceTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/SurrogatesReferenceTests.swift index de5cdf0a8..e9dae8ab7 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/SurrogatesReferenceTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/SurrogatesReferenceTests.swift @@ -1,6 +1,5 @@ // -// TrackerAllowlistReferenceTests.swift -// DuckDuckGo +// SurrogatesReferenceTests.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -32,43 +31,43 @@ final class SurrogatesReferenceTests: XCTestCase { private let tld = TLD() private var redirectTests = [RefTests.Test]() private var webView: WKWebView! - + private enum Resource { static let trackerRadar = "Resources/privacy-reference-tests/tracker-radar-tests/TR-domain-matching/tracker_radar_reference.json" static let tests = "Resources/privacy-reference-tests/tracker-radar-tests/TR-domain-matching/domain_matching_tests.json" static let surrogates = "Resources/privacy-reference-tests/tracker-radar-tests/TR-domain-matching/surrogates.txt" } - + func testSurrogates() throws { let dataLoader = JsonTestDataLoader() - + let trackerRadarJSONData = dataLoader.fromJsonFile(Resource.trackerRadar) let testsData = dataLoader.fromJsonFile(Resource.tests) let surrogatesData = dataLoader.fromJsonFile(Resource.surrogates) - + let referenceTests = try JSONDecoder().decode(RefTests.self, from: testsData) let surrogateTests = referenceTests.surrogateTests.tests - + let surrogateString = String(data: surrogatesData, encoding: .utf8)! - + let trackerData = try JSONDecoder().decode(TrackerData.self, from: trackerRadarJSONData) let encodedData = try? JSONEncoder().encode(trackerData) let encodedTrackerData = String(data: encodedData!, encoding: .utf8)! - + let rules = ContentBlockerRulesBuilder(trackerData: trackerData).buildRules(withExceptions: [], andTemporaryUnprotectedDomains: []) - + let privacyConfig = WebKitTestHelper.preparePrivacyConfig(locallyUnprotected: [], tempUnprotected: [], trackerAllowlist: [:], contentBlockingEnabled: true, exceptions: []) - + let platformTests = surrogateTests.filter { let skip = $0.exceptPlatforms?.contains("ios-browser") return skip == false || skip == nil } - + /* We need to split redirect tests from the rest redirect surrogates have to be injected in webview and then validated against an expression @@ -76,11 +75,11 @@ final class SurrogatesReferenceTests: XCTestCase { redirectTests = platformTests.filter { $0.expectAction == "redirect" } - + let notRedirectTests = platformTests.filter { $0.expectAction != "redirect" } - + for test in notRedirectTests { os_log("TEST: %s", test.name) let requestURL = URL(string: test.requestURL)! @@ -88,36 +87,36 @@ final class SurrogatesReferenceTests: XCTestCase { let requestType = ContentBlockerRulesBuilder.resourceMapping[test.requestType] let rule = rules.matchURL(url: requestURL, topLevel: siteURL, resourceType: requestType!) let result = rule?.action - + if test.expectAction == "block" { XCTAssertEqual(result, .block()) } else if test.expectAction == "ignore" { XCTAssertTrue(result == nil || result == .ignorePreviousRules()) } } - + let testsExecuted = expectation(description: "tests executed") testsExecuted.expectedFulfillmentCount = redirectTests.count - + createWebViewForUserScripTests(trackerData: trackerData, encodedTrackerData: encodedTrackerData, surrogates: surrogateString, privacyConfig: privacyConfig) { webview in - + self.webView = webview self.runTestForRedirect(onTestExecuted: testsExecuted) } - + waitForExpectations(timeout: 30, handler: nil) } - + private func runTestForRedirect(onTestExecuted: XCTestExpectation) { - + guard let test = redirectTests.popLast(), let expectExpression = test.expectExpression else { return } - + os_log("TEST: %s", test.name) let requestURL = URL(string: test.requestURL.testSchemeNormalized)! @@ -134,14 +133,14 @@ final class SurrogatesReferenceTests: XCTestCase { XCTFail("Unknown request type: \(test.requestType) in test \(test.name)") return } - + mockWebsite = MockWebsite(resources: [resource]) - + schemeHandler.reset() schemeHandler.requestHandlers[siteURL] = { _ in return self.mockWebsite.htmlRepresentation.data(using: .utf8)! } - + let request = URLRequest(url: siteURL) WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache, @@ -150,25 +149,25 @@ final class SurrogatesReferenceTests: XCTestCase { completionHandler: { self.webView.load(request) }) - + navigationDelegateMock.onDidFinishNavigation = { - + XCTAssertEqual(self.userScriptDelegateMock.detectedSurrogates.count, 1) - + if let request = self.userScriptDelegateMock.detectedSurrogates.first { XCTAssertTrue(request.isBlocked, "Surrogate should block request \(requestURL)") XCTAssertEqual(request.url, requestURL.absoluteString) } - + self.userScriptDelegateMock.reset() - + self.webView?.evaluateJavaScript(expectExpression, completionHandler: { result, err in XCTAssertNil(err) - + if let result = result as? Bool { XCTAssertTrue(result, "Expression \(expectExpression) should return true") onTestExecuted.fulfill() - + DispatchQueue.main.async { self.runTestForRedirect(onTestExecuted: onTestExecuted) } @@ -176,18 +175,18 @@ final class SurrogatesReferenceTests: XCTestCase { }) } } - + private func createWebViewForUserScripTests(trackerData: TrackerData, encodedTrackerData: String, surrogates: String, privacyConfig: PrivacyConfiguration, completion: @escaping (WKWebView) -> Void) { - + var tempUnprotected = privacyConfig.tempUnprotectedDomains.filter { !$0.trimmingWhitespace().isEmpty } tempUnprotected.append(contentsOf: privacyConfig.exceptionsList(forFeature: .contentBlocking)) - + let exceptions = DefaultContentBlockerRulesExceptionsSource.transform(allowList: privacyConfig.trackerAllowlist.entries) - + WebKitTestHelper.prepareContentBlockingRules(trackerData: trackerData, exceptions: privacyConfig.userUnprotectedDomains, tempUnprotected: tempUnprotected, @@ -196,33 +195,33 @@ final class SurrogatesReferenceTests: XCTestCase { XCTFail("Rules were not compiled properly") return } - + let configuration = WKWebViewConfiguration() configuration.setURLSchemeHandler(self.schemeHandler, forURLScheme: self.schemeHandler.scheme) - + let webView = WKWebView(frame: .init(origin: .zero, size: .init(width: 500, height: 1000)), configuration: configuration) webView.navigationDelegate = self.navigationDelegateMock - + let config = TestSchemeSurrogatesUserScriptConfig(privacyConfig: privacyConfig, surrogates: surrogates, trackerData: trackerData, encodedSurrogateTrackerData: encodedTrackerData, tld: self.tld, isDebugBuild: true) - + let userScript = SurrogatesUserScript(configuration: config) userScript.delegate = self.userScriptDelegateMock - + for messageName in userScript.messageNames { configuration.userContentController.add(userScript, name: messageName) } - + configuration.userContentController.addUserScript(WKUserScript(source: userScript.source, injectionTime: .atDocumentStart, forMainFrameOnly: false)) configuration.userContentController.add(rules) - + completion(webView) } } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/SurrogatesUserScriptTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/SurrogatesUserScriptTests.swift index 5f02c48f4..45194d91e 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/SurrogatesUserScriptTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/SurrogatesUserScriptTests.swift @@ -1,6 +1,5 @@ // // SurrogatesUserScriptTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/TestSchemeHandler.swift b/Tests/BrowserServicesKitTests/ContentBlocker/TestSchemeHandler.swift index ffa364ba3..d8622cb5f 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/TestSchemeHandler.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/TestSchemeHandler.swift @@ -1,6 +1,5 @@ // // TestSchemeHandler.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -26,7 +25,7 @@ final class TestSchemeHandler: NSObject, WKURLSchemeHandler { public var requestHandlers = [URL: RequestResponse]() public let scheme = "test" - + public var genericHandler: RequestResponse = { _ in return Data() } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/TrackerAllowlistReferenceTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/TrackerAllowlistReferenceTests.swift index e502c0eff..b7a2f2523 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/TrackerAllowlistReferenceTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/TrackerAllowlistReferenceTests.swift @@ -1,6 +1,5 @@ // // TrackerAllowlistReferenceTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/TrackerDataManagerTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/TrackerDataManagerTests.swift index 5953fcb54..03ad89539 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/TrackerDataManagerTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/TrackerDataManagerTests.swift @@ -1,6 +1,5 @@ // // TrackerDataManagerTests.swift -// Core // // Copyright © 2019 DuckDuckGo. All rights reserved. // @@ -24,7 +23,7 @@ import TrackerRadarKit import WebKit class TrackerDataManagerTests: XCTestCase { - + static let exampleTDS = """ { "trackers": { @@ -71,9 +70,9 @@ class TrackerDataManagerTests: XCTestCase { } } """ - + func testWhenReloadCalledInitiallyThenDataSetIsEmbedded() { - + let exampleData = Self.exampleTDS.data(using: .utf8)! let embeddedDataProvider = MockEmbeddedDataProvider(data: exampleData, etag: "embedded") @@ -89,7 +88,7 @@ class TrackerDataManagerTests: XCTestCase { let exampleData = Self.exampleTDS.data(using: .utf8)! let embeddedDataProvider = MockEmbeddedDataProvider(data: exampleData, etag: "embedded") - + let trackerDataManager = TrackerDataManager(etag: nil, data: nil, embeddedDataProvider: embeddedDataProvider) @@ -97,12 +96,12 @@ class TrackerDataManagerTests: XCTestCase { XCTAssertNotNil(tracker) XCTAssertEqual("Not Real", tracker?.owner?.displayName) } - + func testFindEntityByName() { let exampleData = Self.exampleTDS.data(using: .utf8)! let embeddedDataProvider = MockEmbeddedDataProvider(data: exampleData, etag: "embedded") - + let trackerDataManager = TrackerDataManager(etag: nil, data: nil, embeddedDataProvider: embeddedDataProvider) @@ -110,21 +109,21 @@ class TrackerDataManagerTests: XCTestCase { XCTAssertNotNil(entity) XCTAssertEqual("Not Real", entity?.displayName) } - + func testFindEntityForHost() { let exampleData = Self.exampleTDS.data(using: .utf8)! let embeddedDataProvider = MockEmbeddedDataProvider(data: exampleData, etag: "embedded") - + let trackerDataManager = TrackerDataManager(etag: nil, data: nil, embeddedDataProvider: embeddedDataProvider) - + let entity = trackerDataManager.embeddedData.tds.findEntity(forHost: "www.notreal.io") XCTAssertNotNil(entity) XCTAssertEqual("Not Real", entity?.displayName) } - + // swiftlint:disable function_body_length func testWhenDownloadedDataAvailableThenReloadUsesIt() { @@ -135,11 +134,11 @@ class TrackerDataManagerTests: XCTestCase { let trackerDataManager = TrackerDataManager(etag: nil, data: nil, embeddedDataProvider: embeddedDataProvider) - + XCTAssertEqual(trackerDataManager.embeddedData.etag, "embedded") XCTAssertEqual(trackerDataManager.reload(etag: "new etag", data: exampleData), TrackerDataManager.ReloadResult.downloaded) - + XCTAssertEqual(trackerDataManager.fetchedData?.etag, "new etag") XCTAssertNil(trackerDataManager.fetchedData?.tds.findEntity(byName: "Google LLC")) XCTAssertNotNil(trackerDataManager.fetchedData?.tds.findEntity(byName: "Not Real")) diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/TrackerResolverTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/TrackerResolverTests.swift index 454a0dd69..977e4df1a 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/TrackerResolverTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/TrackerResolverTests.swift @@ -1,6 +1,5 @@ // // TrackerResolverTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -24,7 +23,7 @@ import ContentBlocking @testable import BrowserServicesKit class TrackerResolverTests: XCTestCase { - + let tld = TLD() func testWhenOptionsAreEmptyThenNothingMatches() { @@ -111,9 +110,9 @@ class TrackerResolverTests: XCTestCase { host: urlThree.host!, resourceType: "image")) } - + func testWhenTrackerIsDetectedThenItIsReported() { - + let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, owner: KnownTracker.Owner(name: "Tracker Inc", @@ -122,20 +121,20 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: ["Advertising"], rules: nil) - + let entity = Entity(displayName: "Trackr Inc company", domains: ["tracker.com"], prevalence: 0.1) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": entity], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: [], tld: tld) - + let result = resolver.trackerFromUrl("https://tracker.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(result) XCTAssert(result?.isBlocked ?? false) XCTAssertEqual(result?.state, .blocked) @@ -144,9 +143,9 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(result?.category, tracker.category) XCTAssertEqual(result?.prevalence, tracker.prevalence) } - + func testWhenTrackerWithBlockActionHasRulesThenTheseAreRespected() { - + let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, owner: KnownTracker.Owner(name: "Tracker Inc", @@ -174,27 +173,27 @@ class TrackerResolverTests: XCTestCase { exceptions: KnownTracker.Rule.Matching(domains: ["other.com"], types: nil)) ]) - + let entity = Entity(displayName: "Trackr Inc company", domains: ["tracker.com"], prevalence: 0.1) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": entity], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: [], tld: tld, adClickAttributionVendor: "attributed.com") - + let blockedImgUrl = resolver.trackerFromUrl("https://tracker.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(blockedImgUrl) XCTAssert(blockedImgUrl?.isBlocked ?? false) XCTAssertEqual(blockedImgUrl?.state, .blocked) @@ -202,12 +201,12 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(blockedImgUrl?.entityName, entity.displayName) XCTAssertEqual(blockedImgUrl?.category, tracker.category) XCTAssertEqual(blockedImgUrl?.prevalence, tracker.prevalence) - + let ignoredTrackerRuleOption = resolver.trackerFromUrl("https://tracker.com/ignore/s.js", pageUrlString: "https://exception.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(ignoredTrackerRuleOption) XCTAssertFalse(ignoredTrackerRuleOption?.isBlocked ?? false) XCTAssertEqual(ignoredTrackerRuleOption?.state, BlockingState.allowed(reason: .ruleException)) @@ -215,12 +214,12 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(ignoredTrackerRuleOption?.entityName, entity.displayName) XCTAssertEqual(ignoredTrackerRuleOption?.category, tracker.category) XCTAssertEqual(ignoredTrackerRuleOption?.prevalence, tracker.prevalence) - + let blockTrackerRuleOption = resolver.trackerFromUrl("https://tracker.com/ignore/s.js", pageUrlString: "https://other.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(blockTrackerRuleOption) XCTAssertFalse(blockTrackerRuleOption?.isBlocked ?? false) XCTAssertEqual(blockTrackerRuleOption?.state, BlockingState.allowed(reason: .ruleException)) @@ -228,12 +227,12 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(blockTrackerRuleOption?.entityName, entity.displayName) XCTAssertEqual(blockTrackerRuleOption?.category, tracker.category) XCTAssertEqual(blockTrackerRuleOption?.prevalence, tracker.prevalence) - + let ignoredTrackerRuleException = resolver.trackerFromUrl("https://tracker.com/nil/s.js", pageUrlString: "https://other.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(ignoredTrackerRuleException) XCTAssertFalse(ignoredTrackerRuleException?.isBlocked ?? false) XCTAssertEqual(ignoredTrackerRuleException?.state, BlockingState.allowed(reason: .ruleException)) @@ -241,12 +240,12 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(ignoredTrackerRuleException?.entityName, entity.displayName) XCTAssertEqual(ignoredTrackerRuleException?.category, tracker.category) XCTAssertEqual(ignoredTrackerRuleException?.prevalence, tracker.prevalence) - + let blockTrackerRuleException = resolver.trackerFromUrl("https://tracker.com/nil/s.js", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(blockTrackerRuleException) XCTAssert(blockTrackerRuleException?.isBlocked ?? false) XCTAssertEqual(blockTrackerRuleException?.state, BlockingState.blocked) @@ -254,12 +253,12 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(blockTrackerRuleException?.entityName, entity.displayName) XCTAssertEqual(blockTrackerRuleException?.category, tracker.category) XCTAssertEqual(blockTrackerRuleException?.prevalence, tracker.prevalence) - + let blockTrackerRuleAttributedException = resolver.trackerFromUrl("https://tracker.com/attr/s.js", pageUrlString: "https://attributed.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(blockTrackerRuleAttributedException) XCTAssertFalse(blockTrackerRuleAttributedException?.isBlocked ?? true) XCTAssertEqual(blockTrackerRuleAttributedException?.state, BlockingState.allowed(reason: .adClickAttribution)) @@ -268,9 +267,9 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(blockTrackerRuleAttributedException?.category, tracker.category) XCTAssertEqual(blockTrackerRuleAttributedException?.prevalence, tracker.prevalence) } - + func testWhenTrackerWithIgnoreActionHasRulesThenTheseAreRespected() { - + let tracker = KnownTracker(domain: "tracker.com", defaultAction: .ignore, owner: KnownTracker.Owner(name: "Tracker Inc", @@ -284,23 +283,23 @@ class TrackerResolverTests: XCTestCase { options: nil, exceptions: KnownTracker.Rule.Matching(domains: ["exception.com"], types: nil))]) - + let entity = Entity(displayName: "Trackr Inc company", domains: ["tracker.com"], prevalence: 0.1) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": entity], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: [], tld: tld) - + let resultImgUrl = resolver.trackerFromUrl("https://tracker.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(resultImgUrl) XCTAssertFalse(resultImgUrl?.isBlocked ?? false) XCTAssertEqual(resultImgUrl?.state, BlockingState.allowed(reason: .ruleException)) @@ -308,12 +307,12 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(resultImgUrl?.entityName, entity.displayName) XCTAssertEqual(resultImgUrl?.category, tracker.category) XCTAssertEqual(resultImgUrl?.prevalence, tracker.prevalence) - + let resultScriptURL = resolver.trackerFromUrl("https://tracker.com/script/s.js", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(resultScriptURL) XCTAssert(resultScriptURL?.isBlocked ?? false) XCTAssertEqual(resultScriptURL?.state, BlockingState.blocked) @@ -321,12 +320,12 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(resultScriptURL?.entityName, entity.displayName) XCTAssertEqual(resultScriptURL?.category, tracker.category) XCTAssertEqual(resultScriptURL?.prevalence, tracker.prevalence) - + let resultScriptURLOnExceptionSite = resolver.trackerFromUrl("https://tracker.com/script/s.js", pageUrlString: "https://exception.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(resultScriptURLOnExceptionSite) XCTAssertFalse(resultScriptURLOnExceptionSite?.isBlocked ?? false) XCTAssertEqual(resultScriptURLOnExceptionSite?.state, BlockingState.allowed(reason: .ruleException)) @@ -335,9 +334,9 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(resultScriptURLOnExceptionSite?.category, tracker.category) XCTAssertEqual(resultScriptURLOnExceptionSite?.prevalence, tracker.prevalence) } - + func testWhenTrackerIsOnAssociatedPageThenItIsNotBlocked() { - + let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, owner: KnownTracker.Owner(name: "Tracker Inc", @@ -346,7 +345,7 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": Entity(displayName: "Tracker Inc company", domains: ["tracker.com", "example.com"], @@ -354,21 +353,21 @@ class TrackerResolverTests: XCTestCase { domains: ["tracker.com": "Tracker Inc", "example.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: [], tld: tld) - + let result = resolver.trackerFromUrl("https://tracker.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(result) XCTAssertFalse(result!.isBlocked) XCTAssertEqual(result?.state, BlockingState.allowed(reason: .ownedByFirstParty)) } - + func testWhenTrackerIsACnameThenItIsReportedAsSuch() { - + let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, owner: KnownTracker.Owner(name: "Tracker Inc", @@ -377,20 +376,20 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let entity = Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], prevalence: 0.1) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": entity], domains: ["tracker.com": "Tracker Inc"], cnames: ["cnamed.com": "tracker.com"]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: [], tld: tld) - + let result = resolver.trackerFromUrl("https://cnamed.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(result) XCTAssert(result?.isBlocked ?? false) XCTAssertEqual(result?.state, BlockingState.blocked) @@ -399,9 +398,9 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(result?.category, tracker.category) XCTAssertEqual(result?.prevalence, tracker.prevalence) } - + func testWhenTrackerIsACnameForAnotherTrackerThenOriginalOneIsReturned() { - + let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, owner: KnownTracker.Owner(name: "Tracker Inc", @@ -410,7 +409,7 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let another = KnownTracker(domain: "another.com", defaultAction: .block, owner: KnownTracker.Owner(name: "Another Inc", @@ -419,15 +418,15 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let trackerEntity = Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], prevalence: 0.1) - + let anotherEntity = Entity(displayName: "Another Inc company", domains: ["another.com"], prevalence: 0.1) - + let tds = TrackerData(trackers: ["tracker.com": tracker, "another.com": another], entities: ["Tracker Inc": trackerEntity, @@ -435,11 +434,11 @@ class TrackerResolverTests: XCTestCase { domains: ["tracker.com": "Tracker Inc", "another.com": "Another Inc."], cnames: ["sub.another.com": "tracker.com"]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: [], tld: tld) - + let result = resolver.trackerFromUrl("https://sub.another.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(result) XCTAssert(result?.isBlocked ?? false) XCTAssertEqual(result?.state, BlockingState.blocked) @@ -448,7 +447,7 @@ class TrackerResolverTests: XCTestCase { XCTAssertEqual(result?.category, another.category) XCTAssertEqual(result?.prevalence, another.prevalence) } - + func testWhenTrackerIsOnUnprotectedSiteItIsNotBlocked() { let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, @@ -458,26 +457,26 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], prevalence: 0.1)], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: ["example.com"], tempList: [], tld: tld) - + let result = resolver.trackerFromUrl("https://tracker.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(result) XCTAssertFalse(result!.isBlocked) XCTAssertEqual(result?.state, BlockingState.allowed(reason: .protectionDisabled)) } - + func testWhenTrackerIsOnTempListItIsNotBlocked() { let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, @@ -487,26 +486,26 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], prevalence: 0.1)], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: ["example.com"], tld: tld) - + let result = resolver.trackerFromUrl("https://tracker.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(result) XCTAssertFalse(result!.isBlocked) XCTAssertEqual(result?.state, BlockingState.allowed(reason: .protectionDisabled)) } - + // This also covers the scenario when tracker is on domain with disabled contentBlocking feature (through temporaryUnprotectedDomains inside ContentBlockerRulesUserScript) func testWhenTrackerIsOnDomainWithDisabledContentBlockingFeatureItIsNotBlocked() { let tracker = KnownTracker(domain: "tracker.com", @@ -517,26 +516,26 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], prevalence: 0.1)], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: ["example.com"], tld: tld) - + let result = resolver.trackerFromUrl("https://tracker.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(result) XCTAssertFalse(result!.isBlocked) XCTAssertEqual(result?.state, BlockingState.allowed(reason: .protectionDisabled)) } - + func testWhenTrackerIsFirstPartyThenItIsNotNotBlocked() { // let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, @@ -546,26 +545,26 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], prevalence: 0.1)], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: ["example.com"], tld: tld) - + let result = resolver.trackerFromUrl("https://tracker.com/img/1.png", pageUrlString: "https://tracker.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNotNil(result) XCTAssertFalse(result!.isBlocked) XCTAssertEqual(result?.state, BlockingState.allowed(reason: .ownedByFirstParty)) } - + func testWhenRequestIsThirdPartyNonTrackerThenItIsIgnored() { // Note: User script has additional logic regarding this case let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, @@ -575,24 +574,24 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], prevalence: 0.1)], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: ["example.com"], tld: tld) - + let result = resolver.trackerFromUrl("https://other.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNil(result) } - + func testWhenRequestIsFirstPartyNonTrackerThenItIsIgnored() { let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, @@ -602,24 +601,24 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], prevalence: 0.1)], domains: ["tracker.com": "Tracker Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: [], tld: tld) - + let result = resolver.trackerFromUrl("https://example.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNil(result) } - + func testWhenRequestIsSameEntityNonTrackerThenItIsIgnored() { let tracker = KnownTracker(domain: "tracker.com", defaultAction: .block, @@ -629,7 +628,7 @@ class TrackerResolverTests: XCTestCase { subdomains: nil, categories: nil, rules: nil) - + let tds = TrackerData(trackers: ["tracker.com": tracker], entities: ["Tracker Inc": Entity(displayName: "Tracker Inc company", domains: ["tracker.com"], @@ -641,15 +640,15 @@ class TrackerResolverTests: XCTestCase { "other.com": "Other Inc", "example.com": "Other Inc"], cnames: [:]) - + let resolver = TrackerResolver(tds: tds, unprotectedSites: [], tempList: [], tld: tld) - + let result = resolver.trackerFromUrl("https://other.com/img/1.png", pageUrlString: "https://example.com", resourceType: "image", potentiallyBlocked: true) - + XCTAssertNil(result) } - + } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/WebViewTestHelper.swift b/Tests/BrowserServicesKitTests/ContentBlocker/WebViewTestHelper.swift index 94e911ecf..126861e54 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/WebViewTestHelper.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/WebViewTestHelper.swift @@ -1,6 +1,5 @@ // // WebViewTestHelper.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -63,7 +62,7 @@ final class MockRulesUserScriptDelegate: NSObject, ContentBlockerRulesUserScript detectedTrackers.insert(tracker) onTrackerDetected?(tracker) } - + func contentBlockerRulesUserScript(_ script: ContentBlockerRulesUserScript, detectedThirdPartyRequest request: DetectedRequest) { detectedThirdPartyRequests.insert(request) diff --git a/Tests/BrowserServicesKitTests/ContentScopeScriptTests/ContentScopePropertiesMocks.swift b/Tests/BrowserServicesKitTests/ContentScopeScriptTests/ContentScopePropertiesMocks.swift index 7bd809979..17790aa22 100644 --- a/Tests/BrowserServicesKitTests/ContentScopeScriptTests/ContentScopePropertiesMocks.swift +++ b/Tests/BrowserServicesKitTests/ContentScopeScriptTests/ContentScopePropertiesMocks.swift @@ -1,10 +1,20 @@ // // ContentScopePropertiesMocks.swift -// // -// Created by Elle Sullivan on 23/05/2022. +// Copyright © 2021 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 @testable import BrowserServicesKit diff --git a/Tests/BrowserServicesKitTests/ContentScopeScriptTests/ContentScopePropertiesTests.swift b/Tests/BrowserServicesKitTests/ContentScopeScriptTests/ContentScopePropertiesTests.swift index fed12a31d..234a45ecc 100644 --- a/Tests/BrowserServicesKitTests/ContentScopeScriptTests/ContentScopePropertiesTests.swift +++ b/Tests/BrowserServicesKitTests/ContentScopeScriptTests/ContentScopePropertiesTests.swift @@ -1,6 +1,5 @@ // // ContentScopePropertiesTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/Email/EmailManagerTests.swift b/Tests/BrowserServicesKitTests/Email/EmailManagerTests.swift index 44a23bf87..fdcdaf7c6 100644 --- a/Tests/BrowserServicesKitTests/Email/EmailManagerTests.swift +++ b/Tests/BrowserServicesKitTests/Email/EmailManagerTests.swift @@ -1,6 +1,5 @@ // // EmailManagerTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/FeatureFlagging/DefaultFeatureFlaggerTests.swift b/Tests/BrowserServicesKitTests/FeatureFlagging/DefaultFeatureFlaggerTests.swift index 0544a25f0..3408cba96 100644 --- a/Tests/BrowserServicesKitTests/FeatureFlagging/DefaultFeatureFlaggerTests.swift +++ b/Tests/BrowserServicesKitTests/FeatureFlagging/DefaultFeatureFlaggerTests.swift @@ -1,6 +1,5 @@ // -// FeatureFlaggerTests.swift -// DuckDuckGo +// DefaultFeatureFlaggerTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -56,7 +55,7 @@ final class DefaultFeatureFlaggerTests: XCTestCase { func testWhenRemoteDevelopment_isInternalUser_whenFeature_returnsPrivacyConfigValue() { internalUserDeciderStore.isInternalUser = true let sourceProvider = FeatureFlagSource.remoteDevelopment(.feature(.autofill)) - + var embeddedData = Self.embeddedConfig(autofillState: "enabled") assertFeatureFlagger(with: embeddedData, willReturn: true, for: sourceProvider) diff --git a/Tests/BrowserServicesKitTests/FeatureFlagging/DefaultInternalUserDeciderTests.swift b/Tests/BrowserServicesKitTests/FeatureFlagging/DefaultInternalUserDeciderTests.swift index f3e1c30ed..31ae5072f 100644 --- a/Tests/BrowserServicesKitTests/FeatureFlagging/DefaultInternalUserDeciderTests.swift +++ b/Tests/BrowserServicesKitTests/FeatureFlagging/DefaultInternalUserDeciderTests.swift @@ -1,6 +1,5 @@ // -// FeatureFlaggingTests.swift -// DuckDuckGo +// DefaultInternalUserDeciderTests.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -21,10 +20,10 @@ import XCTest @testable import BrowserServicesKit class DefaultInternalUserDeciderTests: XCTestCase { - + let correctURL = URL(string: "http://use-login.duckduckgo.com")! let correctStatusCode = 200 - + func testShouldMarkUserAsInternalWhenURLAndStatusCodeCorrectThenReturnsTrue() { let featureFlagger = DefaultInternalUserDecider() let result = featureFlagger.shouldMarkUserAsInternal(forUrl: correctURL, statusCode: correctStatusCode) diff --git a/Tests/BrowserServicesKitTests/Fingerprinting/FingerprintingReferenceTests.swift b/Tests/BrowserServicesKitTests/Fingerprinting/FingerprintingReferenceTests.swift index e6cbed6da..982fcd774 100644 --- a/Tests/BrowserServicesKitTests/Fingerprinting/FingerprintingReferenceTests.swift +++ b/Tests/BrowserServicesKitTests/Fingerprinting/FingerprintingReferenceTests.swift @@ -1,6 +1,5 @@ // // FingerprintingReferenceTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -32,23 +31,23 @@ final class FingerprintingReferenceTests: XCTestCase { private let dataLoader = JsonTestDataLoader() private var webView: WKWebView! private var mockWebsite: MockWebsite! - + private enum Resource { static let script = "Resources/privacy-reference-tests/fingerprinting-protections/init.js" static let config = "Resources/privacy-reference-tests/fingerprinting-protections/config_reference.json" static let tests = "Resources/privacy-reference-tests/fingerprinting-protections/tests.json" } - + private lazy var testData: TestData = { let testData = dataLoader.fromJsonFile(Resource.tests) return try! JSONDecoder().decode(TestData.self, from: testData) }() - + private lazy var scriptToInject: String = { let scriptData = dataLoader.fromJsonFile(Resource.script) return String(data: scriptData, encoding: .utf8)! }() - + private lazy var privacyManager: PrivacyConfigurationManager = { let configJSONData = dataLoader.fromJsonFile(Resource.config) let embeddedDataProvider = MockEmbeddedDataProvider(data: configJSONData, @@ -61,57 +60,57 @@ final class FingerprintingReferenceTests: XCTestCase { localProtection: localProtection, internalUserDecider: DefaultInternalUserDecider()) }() - + override func tearDown() { super.tearDown() referenceTests.removeAll() } - + func testBatteryAPI() throws { let sectionName = testData.batteryAPI.name - + referenceTests = testData.batteryAPI.tests.filter { return $0.exceptPlatforms.contains("ios-browser") == false } - + guard referenceTests.count > 0 else { os_log("NO TESTS FOR SECTION: %s", sectionName) return } - + os_log("TEST SECTION: %s", sectionName) - + let testsExecuted = expectation(description: "tests executed") testsExecuted.expectedFulfillmentCount = referenceTests.count - + runTests(onTestExecuted: testsExecuted) waitForExpectations(timeout: 30, handler: nil) } - + func testHardwareAPI() throws { let sectionName = testData.hardwareAPIs.name - + referenceTests = testData.hardwareAPIs.tests.filter { return $0.exceptPlatforms.contains("ios-browser") == false } - + guard referenceTests.count > 0 else { os_log("NO TESTS FOR SECTION: %s", sectionName) return } - + os_log("TEST SECTION: %s", sectionName) - + let testsExecuted = expectation(description: "tests executed") testsExecuted.expectedFulfillmentCount = referenceTests.count - + runTests(onTestExecuted: testsExecuted) waitForExpectations(timeout: 30, handler: nil) } - + func testScreenAPI() throws { let sectionName = testData.screenAPI.name - + referenceTests = testData.screenAPI.tests.filter { return $0.exceptPlatforms.contains("ios-browser") == false } @@ -119,64 +118,64 @@ final class FingerprintingReferenceTests: XCTestCase { os_log("NO TESTS FOR SECTION: %s", sectionName) return } - + os_log("TEST SECTION: %s", sectionName) - + let testsExecuted = expectation(description: "tests executed") testsExecuted.expectedFulfillmentCount = referenceTests.count - + runTests(onTestExecuted: testsExecuted) waitForExpectations(timeout: 30, handler: nil) } - + func testStorageAPI() throws { let sectionName = testData.temporaryStorageAPI.name - + referenceTests = testData.temporaryStorageAPI.tests.filter { return $0.exceptPlatforms.contains("ios-browser") == false } - + guard referenceTests.count > 0 else { os_log("NO TESTS FOR SECTION: %s", sectionName) return } - + os_log("TEST SECTION: %s", sectionName) - + let testsExecuted = expectation(description: "tests executed") testsExecuted.expectedFulfillmentCount = referenceTests.count - + runTests(onTestExecuted: testsExecuted) waitForExpectations(timeout: 30, handler: nil) } - + private func runTests(onTestExecuted: XCTestExpectation) { - + guard let test = referenceTests.popLast(), test.exceptPlatforms.contains("macos-browser") == false else { return } - + os_log("TEST: %s", test.name) - + let requestURL = URL(string: test.siteURL.testSchemeNormalized)! - + schemeHandler.reset() schemeHandler.requestHandlers[requestURL] = { _ in return "".data(using: .utf8)! } - + let request = URLRequest(url: requestURL) - + setupWebViewForUserScripTests(schemeHandler: schemeHandler, privacyConfig: privacyManager.privacyConfig) { webView in // Keep webview in memory till test finishes self.webView = webView self.webView.load(request) } - + navigationDelegateMock.onDidFinishNavigation = { [weak self] in - + self!.webView.evaluateJavaScript(test.property) { result, _ in if let result = result as? String { XCTAssertEqual(result, test.expectPropertyValue, "Values should be equal for test: \(test.name)") @@ -188,7 +187,7 @@ final class FingerprintingReferenceTests: XCTestCase { } else { XCTFail("Should not return nil \(test.name)") } - + DispatchQueue.main.async { onTestExecuted.fulfill() self!.runTests(onTestExecuted: onTestExecuted) @@ -196,17 +195,17 @@ final class FingerprintingReferenceTests: XCTestCase { } } } - + private func setupWebViewForUserScripTests(schemeHandler: TestSchemeHandler, privacyConfig: PrivacyConfiguration, completion: @escaping (WKWebView) -> Void) { let configuration = WKWebViewConfiguration() configuration.setURLSchemeHandler(schemeHandler, forURLScheme: schemeHandler.scheme) - + let webView = WKWebView(frame: .init(origin: .zero, size: .init(width: 500, height: 1000)), configuration: configuration) webView.navigationDelegate = self.navigationDelegateMock - + let configFeatureToggle = ContentScopeFeatureToggles(emailProtection: false, emailProtectionIncontextSignup: false, credentialsAutofill: false, @@ -216,28 +215,28 @@ final class FingerprintingReferenceTests: XCTestCase { passwordGeneration: false, inlineIconCredentials: false, thirdPartyCredentialsProvider: false) - + let contentScopeProperties = ContentScopeProperties(gpcEnabled: false, sessionKey: UUID().uuidString, featureToggles: configFeatureToggle) - + let contentScopeScript = ContentScopeUserScript(self.privacyManager, properties: contentScopeProperties) - + configuration.userContentController.addUserScript(WKUserScript(source: "\(scriptToInject) init(window)", injectionTime: .atDocumentStart, forMainFrameOnly: false)) - + configuration.userContentController.addUserScript(WKUserScript(source: contentScopeScript.source, injectionTime: .atDocumentStart, forMainFrameOnly: false)) - + for messageName in contentScopeScript.messageNames { configuration.userContentController.add(contentScopeScript, name: messageName) } - + completion(webView) - + } } diff --git a/Tests/BrowserServicesKitTests/GPC/GPCReferenceTests.swift b/Tests/BrowserServicesKitTests/GPC/GPCReferenceTests.swift index 61b26ec60..8fe83c91f 100644 --- a/Tests/BrowserServicesKitTests/GPC/GPCReferenceTests.swift +++ b/Tests/BrowserServicesKitTests/GPC/GPCReferenceTests.swift @@ -1,6 +1,5 @@ // // GPCReferenceTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -48,44 +47,44 @@ final class GPCReferenceTests: XCTestCase { localProtection: localProtection, internalUserDecider: DefaultInternalUserDecider()) } - + func testGPCHeader() throws { let dataLoader = JsonTestDataLoader() let testsData = dataLoader.fromJsonFile(Resource.tests) let referenceTests = try JSONDecoder().decode(GPCTestData.self, from: testsData) - + let privacyConfig = privacyManager.privacyConfig for test in referenceTests.gpcHeader.tests { - + if test.exceptPlatforms.contains("ios-browser") || test.exceptPlatforms.contains("macos-browser") { os_log("Skipping test, ignore platform for [%s]", type: .info, test.name) continue } - + os_log("Testing [%s]", type: .info, test.name) - + let factory = GPCRequestFactory() var testRequest = URLRequest(url: URL(string: test.requestURL)!) - + // Simulate request with actual headers testRequest.addValue("DDG-Test", forHTTPHeaderField: "User-Agent") let request = factory.requestForGPC(basedOn: testRequest, config: privacyConfig, gpcEnabled: test.gpcUserSettingOn) - + if !test.gpcUserSettingOn { XCTAssertNil(request, "User opt out, request should not exist \([test.name])") } - + let hasHeader = request?.allHTTPHeaderFields?[GPCRequestFactory.Constants.secGPCHeader] != nil let headerValue = request?.allHTTPHeaderFields?[GPCRequestFactory.Constants.secGPCHeader] if test.expectGPCHeader { XCTAssertNotNil(request, "Request should exist if expectGPCHeader is true [\(test.name)]") XCTAssert(hasHeader, "Couldn't find header for [\(test.requestURL)]") - + if let expectedHeaderValue = test.expectGPCHeaderValue { let headerValue = request?.allHTTPHeaderFields?[GPCRequestFactory.Constants.secGPCHeader] XCTAssertEqual(expectedHeaderValue, headerValue, "Header should be equal [\(test.name)]") @@ -95,41 +94,41 @@ final class GPCReferenceTests: XCTestCase { } } } - + func testGPCJavascriptAPI() throws { let dataLoader = JsonTestDataLoader() let testsData = dataLoader.fromJsonFile(Resource.tests) let referenceTests = try JSONDecoder().decode(GPCTestData.self, from: testsData) - + javascriptTests = referenceTests.gpcJavaScriptAPI.tests.filter { $0.exceptPlatforms.contains("macos-browser") == false } - + let testsExecuted = expectation(description: "tests executed") testsExecuted.expectedFulfillmentCount = javascriptTests.count - + runJavascriptTests(onTestExecuted: testsExecuted) - + waitForExpectations(timeout: 30, handler: nil) } - + private func runJavascriptTests(onTestExecuted: XCTestExpectation) { - + guard let test = javascriptTests.popLast() else { return } - + let siteURL = URL(string: test.siteURL.testSchemeNormalized)! - + schemeHandler.reset() schemeHandler.requestHandlers[siteURL] = { _ in return "".data(using: .utf8)! } - + let request = URLRequest(url: siteURL) let webView = createWebViewForUserScripTests(gpcEnabled: test.gpcUserSettingOn, privacyConfig: privacyManager.privacyConfig) - + WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache, WKWebsiteDataTypeOfflineWebApplicationCache], @@ -137,15 +136,15 @@ final class GPCReferenceTests: XCTestCase { completionHandler: { webView.load(request) }) - + let javascriptToEvaluate = "Navigator.prototype.globalPrivacyControl" - + navigationDelegateMock.onDidFinishNavigation = { - + webView.evaluateJavaScript(javascriptToEvaluate, completionHandler: { result, err in - + XCTAssertNil(err, "Evaluation should not fail") - + if let expectedValue = test.expectGPCAPIValue { switch expectedValue { case "false": @@ -156,7 +155,7 @@ final class GPCReferenceTests: XCTestCase { XCTAssertNil(result, "Test \(test.name) expected value should be nil") } } - + DispatchQueue.main.async { onTestExecuted.fulfill() self.runJavascriptTests(onTestExecuted: onTestExecuted) @@ -164,31 +163,31 @@ final class GPCReferenceTests: XCTestCase { }) } } - + private func createWebViewForUserScripTests(gpcEnabled: Bool, privacyConfig: PrivacyConfiguration) -> WKWebView { - + let properties = ContentScopeProperties(gpcEnabled: gpcEnabled, sessionKey: UUID().uuidString, featureToggles: ContentScopeFeatureToggles.allTogglesOn) - + let contentScopeScript = ContentScopeUserScript(privacyManager, properties: properties) - + let configuration = WKWebViewConfiguration() configuration.setURLSchemeHandler(self.schemeHandler, forURLScheme: self.schemeHandler.scheme) - + let webView = WKWebView(frame: .init(origin: .zero, size: .init(width: 500, height: 1000)), configuration: configuration) webView.navigationDelegate = self.navigationDelegateMock - + for messageName in contentScopeScript.messageNames { configuration.userContentController.add(contentScopeScript, name: messageName) } - + configuration.userContentController.addUserScript(WKUserScript(source: contentScopeScript.source, injectionTime: .atDocumentStart, forMainFrameOnly: false)) - + return webView } } diff --git a/Tests/BrowserServicesKitTests/GPC/GPCTests.swift b/Tests/BrowserServicesKitTests/GPC/GPCTests.swift index 6d07ad500..b83a30e8a 100644 --- a/Tests/BrowserServicesKitTests/GPC/GPCTests.swift +++ b/Tests/BrowserServicesKitTests/GPC/GPCTests.swift @@ -1,6 +1,5 @@ // // GPCTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -25,7 +24,7 @@ final class GPCTests: XCTestCase { override func setUp() { super.setUp() - + let gpcFeature = PrivacyConfigurationData.PrivacyFeature(state: "enabled", exceptions: [], settings: [ @@ -41,32 +40,32 @@ final class GPCTests: XCTestCase { let localProtection = MockDomainsProtectionStore() appConfig = AppPrivacyConfiguration(data: privacyData, identifier: "", localProtection: localProtection, internalUserDecider: DefaultInternalUserDecider()) } - + func testWhenGPCEnableDomainIsHttpThenISGPCEnabledTrue() { let result = GPCRequestFactory().isGPCEnabled(url: URL(string: "https://www.washingtonpost.com")!, config: appConfig) XCTAssertTrue(result) } - + func testWhenGPCEnableDomainIsHttpsThenISGPCEnabledTrue() { let result = GPCRequestFactory().isGPCEnabled(url: URL(string: "http://www.washingtonpost.com")!, config: appConfig) XCTAssertTrue(result) } - + func testWhenGPCEnableDomainHasNoSubDomainThenISGPCEnabledTrue() { let result = GPCRequestFactory().isGPCEnabled(url: URL(string: "http://washingtonpost.com")!, config: appConfig) XCTAssertTrue(result) } - + func testWhenGPCEnableDomainHasPathThenISGPCEnabledTrue() { let result = GPCRequestFactory().isGPCEnabled(url: URL(string: "http://www.washingtonpost.com/test/somearticle.html")!, config: appConfig) XCTAssertTrue(result) } - + func testWhenGPCEnableDomainHasCorrectSubdomainThenISGPCEnabledTrue() { let result = GPCRequestFactory().isGPCEnabled(url: URL(string: "http://global-privacy-control.glitch.me")!, config: appConfig) XCTAssertTrue(result) } - + func testWhenGPCEnableDomainHasWrongSubdomainThenISGPCEnabledFalse() { let result = GPCRequestFactory().isGPCEnabled(url: URL(string: "http://glitch.me")!, config: appConfig) XCTAssertFalse(result) diff --git a/Tests/BrowserServicesKitTests/InternalUserDecider/MockInternalUserStoring.swift b/Tests/BrowserServicesKitTests/InternalUserDecider/MockInternalUserStoring.swift index c232a758a..208160f7e 100644 --- a/Tests/BrowserServicesKitTests/InternalUserDecider/MockInternalUserStoring.swift +++ b/Tests/BrowserServicesKitTests/InternalUserDecider/MockInternalUserStoring.swift @@ -1,6 +1,5 @@ // // MockInternalUserStoring.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/LinkProtection/AmpMatchingTests.swift b/Tests/BrowserServicesKitTests/LinkProtection/AmpMatchingTests.swift index 4b8da9100..e4a2bb3d9 100644 --- a/Tests/BrowserServicesKitTests/LinkProtection/AmpMatchingTests.swift +++ b/Tests/BrowserServicesKitTests/LinkProtection/AmpMatchingTests.swift @@ -1,6 +1,5 @@ // // AmpMatchingTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -28,41 +27,41 @@ struct AmpRefTests: Decodable { let desc: String let tests: [AmpFormatTest] } - + struct AmpFormatTest: Decodable { let name: String let ampURL: String let expectURL: String let exceptPlatforms: [String]? } - + struct AmpKeywordTests: Decodable { let name: String let desc: String let tests: [AmpKeywordTest] } - + struct AmpKeywordTest: Decodable { let name: String let ampURL: String let expectAmpDetected: Bool let exceptPlatforms: [String]? } - + let ampFormats: AmpFormatTests let ampKeywords: AmpKeywordTests } final class AmpMatchingTests: XCTestCase { - + private enum Resource { static let config = "Resources/privacy-reference-tests/amp-protections/config_reference.json" static let tests = "Resources/privacy-reference-tests/amp-protections/tests.json" } - + private static let data = JsonTestDataLoader() private static let config = data.fromJsonFile(Resource.config) - + private var privacyManager: PrivacyConfigurationManager { let embeddedDataProvider = MockEmbeddedDataProvider(data: Self.config, etag: "embedded") @@ -75,58 +74,58 @@ final class AmpMatchingTests: XCTestCase { localProtection: localProtection, internalUserDecider: DefaultInternalUserDecider()) } - + private var contentBlockingManager: ContentBlockerRulesManager { let listsSource = ContentBlockerRulesListSourceMock() let exceptionsSource = ContentBlockerRulesExceptionsSourceMock() return ContentBlockerRulesManager(rulesSource: listsSource, exceptionsSource: exceptionsSource) } - + private lazy var ampTestSuite: AmpRefTests = { let tests = Self.data.fromJsonFile(Resource.tests) return try! JSONDecoder().decode(AmpRefTests.self, from: tests) }() - + func testAmpFormats() throws { let tests = ampTestSuite.ampFormats.tests let linkCleaner = LinkCleaner(privacyManager: privacyManager) - + for test in tests { let skip = test.exceptPlatforms?.contains("ios-browser") if skip == true { os_log("!!SKIPPING TEST: %s", test.name) continue } - + os_log("TEST: %s", test.name) - + let ampUrl = URL(string: test.ampURL) let resultUrl = linkCleaner.extractCanonicalFromAMPLink(initiator: nil, destination: ampUrl) - + // Empty expectedUrl should be treated as nil let expectedUrl = !test.expectURL.isEmpty ? test.expectURL : nil XCTAssertEqual(resultUrl?.absoluteString, expectedUrl, "\(resultUrl!.absoluteString) not equal to expected: \(expectedUrl ?? "nil")") } } - + func testAmpKeywords() throws { let tests = ampTestSuite.ampKeywords.tests let linkCleaner = LinkCleaner(privacyManager: privacyManager) - + let ampExtractor = AMPCanonicalExtractor(linkCleaner: linkCleaner, privacyManager: privacyManager, contentBlockingManager: contentBlockingManager, errorReporting: nil) - + for test in tests { let skip = test.exceptPlatforms?.contains("ios-browser") if skip == true { os_log("!!SKIPPING TEST: %s", test.name) continue } - + os_log("TEST: %s", test.name) - + let ampUrl = URL(string: test.ampURL) let result = ampExtractor.urlContainsAMPKeyword(ampUrl) XCTAssertEqual(result, test.expectAmpDetected, "\(test.ampURL) not correctly identified. Expected: \(test.expectAmpDetected.description)") diff --git a/Tests/BrowserServicesKitTests/LinkProtection/ContentBlockerManagerMock.swift b/Tests/BrowserServicesKitTests/LinkProtection/ContentBlockerManagerMock.swift index 044a95324..b701b2be3 100644 --- a/Tests/BrowserServicesKitTests/LinkProtection/ContentBlockerManagerMock.swift +++ b/Tests/BrowserServicesKitTests/LinkProtection/ContentBlockerManagerMock.swift @@ -1,6 +1,5 @@ // -// AmpMatchingTests.swift -// DuckDuckGo +// ContentBlockerManagerMock.swift // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/LinkProtection/URLParameterTests.swift b/Tests/BrowserServicesKitTests/LinkProtection/URLParameterTests.swift index 3b32c166b..e6e2a3e73 100644 --- a/Tests/BrowserServicesKitTests/LinkProtection/URLParameterTests.swift +++ b/Tests/BrowserServicesKitTests/LinkProtection/URLParameterTests.swift @@ -1,6 +1,5 @@ // // URLParameterTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -28,7 +27,7 @@ struct URLParamRefTests: Decodable { let desc: String let tests: [URLParamTest] } - + struct URLParamTest: Decodable { let name: String let testURL: String @@ -36,20 +35,20 @@ struct URLParamRefTests: Decodable { let initiatorURL: String? let exceptPlatforms: [String]? } - + let trackingParameters: URLParamTests } final class URLParameterTests: XCTestCase { - + private enum Resource { static let config = "Resources/privacy-reference-tests/url-parameters/config_reference.json" static let tests = "Resources/privacy-reference-tests/url-parameters/tests.json" } - + private static let data = JsonTestDataLoader() private static let config = data.fromJsonFile(Resource.config) - + private var privacyManager: PrivacyConfigurationManager { let embeddedDataProvider = MockEmbeddedDataProvider(data: Self.config, etag: "embedded") @@ -62,35 +61,35 @@ final class URLParameterTests: XCTestCase { localProtection: localProtection, internalUserDecider: DefaultInternalUserDecider()) } - + private lazy var urlParamTestSuite: URLParamRefTests = { let tests = Self.data.fromJsonFile(Resource.tests) return try! JSONDecoder().decode(URLParamRefTests.self, from: tests) }() - + func testURLParamStripping() throws { let tests = urlParamTestSuite.trackingParameters.tests - + let linkCleaner = LinkCleaner(privacyManager: privacyManager) - + for test in tests { let skip = test.exceptPlatforms?.contains("ios-browser") if skip == true { os_log("!!SKIPPING TEST: %s", test.name) continue } - + os_log("TEST: %s", test.name) - + let testUrl = URL(string: test.testURL) let initiator = test.initiatorURL != nil ? URL(string: test.initiatorURL!) : nil var resultUrl = linkCleaner.cleanTrackingParameters(initiator: initiator, url: testUrl) - + if resultUrl == nil { // Tests expect unchanged URLs to match testURL resultUrl = testUrl } - + XCTAssertEqual(resultUrl?.absoluteString, test.expectURL, "\(resultUrl?.absoluteString ?? "(nil)") not equal to expected: \(test.expectURL)") } diff --git a/Tests/BrowserServicesKitTests/PrivacyConfig/AdClickAttributionFeatureTests.swift b/Tests/BrowserServicesKitTests/PrivacyConfig/AdClickAttributionFeatureTests.swift index fe8a2344b..50530f743 100644 --- a/Tests/BrowserServicesKitTests/PrivacyConfig/AdClickAttributionFeatureTests.swift +++ b/Tests/BrowserServicesKitTests/PrivacyConfig/AdClickAttributionFeatureTests.swift @@ -1,6 +1,5 @@ // // AdClickAttributionFeatureTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -21,7 +20,7 @@ import XCTest import BrowserServicesKit class AdClickAttributionFeatureTests: XCTestCase { - + let exampleConfig = """ { "readme": "https://github.com/duckduckgo/privacy-configuration", @@ -66,37 +65,37 @@ class AdClickAttributionFeatureTests: XCTestCase { ] } """.data(using: .utf8)! - + func testDomainMatching() { let dataProvider = MockEmbeddedDataProvider(data: exampleConfig, etag: "empty") - + let config = PrivacyConfigurationManager(fetchedETag: nil, fetchedData: nil, embeddedDataProvider: dataProvider, localProtection: MockDomainsProtectionStore(), internalUserDecider: DefaultInternalUserDecider()) - + let feature = AdClickAttributionFeature(with: config) - + XCTAssertTrue(feature.isEnabled) XCTAssertEqual(Set(feature.allowlist.map { $0.entity }), Set(["bing.com", "ad-site.site", "ad-site.example"])) - + XCTAssertTrue(feature.isMatchingAttributionFormat(URL(string: "https://good.first-party.site/y.js?test_param=test")!)) - + XCTAssertFalse(feature.isMatchingAttributionFormat(URL(string: "https://good.first-party.site/y.js")!)) XCTAssertFalse(feature.isMatchingAttributionFormat(URL(string: "https://good.first-party.site/y.js?u2=2")!)) XCTAssertFalse(feature.isMatchingAttributionFormat(URL(string: "https://good.first-party.site/y.js.gif?u2=2")!)) XCTAssertFalse(feature.isMatchingAttributionFormat(URL(string: "https://sub.good.first-party.site/y.js?u3=2")!)) - + // No ad domain param XCTAssertFalse(feature.isMatchingAttributionFormat(URL(string: "https://good.first-party.example/y.js?test_param=test.com")!)) - + // Testing for hardcoded value XCTAssertFalse(feature.isMatchingAttributionFormat(URL(string: "https://other.first-party.com/m.js?ad_domain=a.com")!)) XCTAssertFalse(feature.isMatchingAttributionFormat(URL(string: "https://other.first-party.com/m.js?test_param=test.com")!)) - + // Dropping parameters tests XCTAssertTrue(feature.isMatchingAttributionFormat(URL(string: "https://different.party.com/y.js?test_param=&foo=&bar=")!)) XCTAssertTrue(feature.isMatchingAttributionFormat(URL(string: "https://different.party.com/y.js?test_param=example.com&foo=&bar=")!)) diff --git a/Tests/BrowserServicesKitTests/PrivacyConfig/AppPrivacyConfigurationTests.swift b/Tests/BrowserServicesKitTests/PrivacyConfig/AppPrivacyConfigurationTests.swift index 97689d3de..1765a015d 100644 --- a/Tests/BrowserServicesKitTests/PrivacyConfig/AppPrivacyConfigurationTests.swift +++ b/Tests/BrowserServicesKitTests/PrivacyConfig/AppPrivacyConfigurationTests.swift @@ -1,6 +1,5 @@ // // AppPrivacyConfigurationTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -651,7 +650,7 @@ class AppPrivacyConfigurationTests: XCTestCase { XCTAssertTrue(config.isEnabled(featureKey: .autofill, versionProvider: currentVersionProvider)) XCTAssertTrue(config.isSubfeatureEnabled(AutofillSubfeature.credentialsSaving, versionProvider: currentVersionProvider, randomizer: Double.random(in:))) } - + let exampleSubfeatureWithRolloutsConfig = """ { @@ -674,17 +673,17 @@ class AppPrivacyConfigurationTests: XCTestCase { "unprotectedTemporary": [] } """.data(using: .utf8)! - + func clearRolloutData(feature: String, subFeature: String) { UserDefaults().set(nil, forKey: "config.\(feature).\(subFeature).enabled") UserDefaults().set(nil, forKey: "config.\(feature).\(subFeature).lastRolloutCount") } - + var mockRandomValue: Double = 0.0 func mockRandom(in range: Range) -> Double { return mockRandomValue } - + func testWhenCheckingSubfeatureState_SubfeatureIsEnabledWithSingleRolloutProbability() { let mockEmbeddedData = MockEmbeddedDataProvider(data: exampleSubfeatureWithRolloutsConfig, etag: "test") let manager = PrivacyConfigurationManager(fetchedETag: nil, @@ -694,18 +693,18 @@ class AppPrivacyConfigurationTests: XCTestCase { internalUserDecider: DefaultInternalUserDecider()) let config = manager.privacyConfig - + mockRandomValue = 7.0 clearRolloutData(feature: "autofill", subFeature: "credentialsSaving") var enabled = config.isSubfeatureEnabled(AutofillSubfeature.credentialsSaving, randomizer: mockRandom(in:)) XCTAssertFalse(enabled, "Feature should not be enabled if selected value above rollout") - + mockRandomValue = 2.0 clearRolloutData(feature: "autofill", subFeature: "credentialsSaving") enabled = config.isSubfeatureEnabled(AutofillSubfeature.credentialsSaving, randomizer: mockRandom(in:)) XCTAssertTrue(enabled, "Feature should be enabled if selected value below rollout") } - + let exampleSubfeatureWithMultipleRolloutsConfig = """ { @@ -758,7 +757,7 @@ class AppPrivacyConfigurationTests: XCTestCase { "unprotectedTemporary": [] } """.data(using: .utf8)! - + func testWhenCheckingSubfeatureState_SubfeatureIsEnabledWithMultipleRolloutProbability() { let mockEmbeddedData = MockEmbeddedDataProvider(data: exampleSubfeatureWithMultipleRolloutsConfig, etag: "test") let manager = PrivacyConfigurationManager(fetchedETag: nil, @@ -768,28 +767,28 @@ class AppPrivacyConfigurationTests: XCTestCase { internalUserDecider: DefaultInternalUserDecider()) let config = manager.privacyConfig - + mockRandomValue = 37 clearRolloutData(feature: "autofill", subFeature: "credentialsSaving") var enabled = config.isSubfeatureEnabled(AutofillSubfeature.credentialsSaving, randomizer: mockRandom(in:)) XCTAssertFalse(enabled, "Feature should not be enabled if selected value above rollout") - + mockRandomValue = 0.1 // Effective probability of 10.5% in test config clearRolloutData(feature: "autofill", subFeature: "credentialsSaving") enabled = config.isSubfeatureEnabled(AutofillSubfeature.credentialsSaving, randomizer: mockRandom(in:)) XCTAssertTrue(enabled, "Feature should not be enabled if selected value above rollout") - + mockRandomValue = 37 clearRolloutData(feature: "autofill", subFeature: "credentialsAutofill") enabled = config.isSubfeatureEnabled(AutofillSubfeature.credentialsAutofill, randomizer: mockRandom(in:)) XCTAssertFalse(enabled, "Feature should not be enabled if selected value above rollout") - + mockRandomValue = 0.10 // Effective probability of 11.7% in test config clearRolloutData(feature: "autofill", subFeature: "credentialsAutofill") enabled = config.isSubfeatureEnabled(AutofillSubfeature.credentialsAutofill, randomizer: mockRandom(in:)) XCTAssertTrue(enabled, "Feature should not be enabled if selected value above rollout") } - + func testWhenCheckingSubfeatureStateAndRolloutSizeChanges_SubfeatureIsEnabledWithMultipleRolloutProbability() { let mockEmbeddedData = MockEmbeddedDataProvider(data: exampleSubfeatureWithMultipleRolloutsConfig, etag: "test") let manager = PrivacyConfigurationManager(fetchedETag: nil, @@ -799,7 +798,7 @@ class AppPrivacyConfigurationTests: XCTestCase { internalUserDecider: DefaultInternalUserDecider()) let config = manager.privacyConfig - + clearRolloutData(feature: "autofill", subFeature: "credentialsAutofill") mockRandomValue = 0.10 // Mock that the user has previously seen the rollout and was not chosen @@ -807,7 +806,7 @@ class AppPrivacyConfigurationTests: XCTestCase { var enabled = config.isSubfeatureEnabled(AutofillSubfeature.credentialsAutofill, randomizer: mockRandom(in:)) XCTAssert(enabled, "Subfeature should be enabled when rollout count changes") - + clearRolloutData(feature: "autofill", subFeature: "credentialsAutofill") // Mock that the user has previously seen the rollout and was not chosen UserDefaults().set(3, forKey: "config.autofill.credentialsAutofill.lastRolloutCount") @@ -815,7 +814,7 @@ class AppPrivacyConfigurationTests: XCTestCase { XCTAssertFalse(enabled, "Subfeature should not be enabled when rollout count does not changes") } - + func testWhenCheckingSubfeatureStateAndUserIsInARollout_SubfeatureIsEnabled() { let mockEmbeddedData = MockEmbeddedDataProvider(data: exampleSubfeatureWithMultipleRolloutsConfig, etag: "test") let manager = PrivacyConfigurationManager(fetchedETag: nil, @@ -823,14 +822,14 @@ class AppPrivacyConfigurationTests: XCTestCase { embeddedDataProvider: mockEmbeddedData, localProtection: MockDomainsProtectionStore(), internalUserDecider: DefaultInternalUserDecider()) - + let config = manager.privacyConfig - + clearRolloutData(feature: "autofill", subFeature: "credentialsAutofill") UserDefaults().set(true, forKey: "config.autofill.credentialsAutofill.enabled") XCTAssert(config.isSubfeatureEnabled(AutofillSubfeature.credentialsAutofill), "Subfeature should be enabled if the user has already been selected in a rollout") } - + func testWhenCheckingSubfeatureStateAndRolloutsIsEmpty_SubfeatrueIsEnabled() { let mockEmbeddedData = MockEmbeddedDataProvider(data: exampleSubfeatureWithMultipleRolloutsConfig, etag: "test") let manager = PrivacyConfigurationManager(fetchedETag: nil, @@ -838,13 +837,13 @@ class AppPrivacyConfigurationTests: XCTestCase { embeddedDataProvider: mockEmbeddedData, localProtection: MockDomainsProtectionStore(), internalUserDecider: DefaultInternalUserDecider()) - + let config = manager.privacyConfig - + clearRolloutData(feature: "autofill", subFeature: "inlineIconCredentials") XCTAssert(config.isSubfeatureEnabled(AutofillSubfeature.inlineIconCredentials), "Subfeature should be enabled if rollouts array is empty") } - + func testWhenCheckingSubfeatureStateWithRolloutsAndSubfeatureDisabled_SubfeatureShouldBeDisabled() { let mockEmbeddedData = MockEmbeddedDataProvider(data: exampleSubfeatureWithMultipleRolloutsConfig, etag: "test") let manager = PrivacyConfigurationManager(fetchedETag: nil, @@ -852,9 +851,9 @@ class AppPrivacyConfigurationTests: XCTestCase { embeddedDataProvider: mockEmbeddedData, localProtection: MockDomainsProtectionStore(), internalUserDecider: DefaultInternalUserDecider()) - + let config = manager.privacyConfig - + clearRolloutData(feature: "autofill", subFeature: "accessCredentialManagement") XCTAssertFalse(config.isSubfeatureEnabled(AutofillSubfeature.accessCredentialManagement), "Subfeature should be enabled if rollouts array is empty") } diff --git a/Tests/BrowserServicesKitTests/PrivacyConfig/PrivacyConfigurationDataTests.swift b/Tests/BrowserServicesKitTests/PrivacyConfig/PrivacyConfigurationDataTests.swift index a0025d3c9..94fcd1b6e 100644 --- a/Tests/BrowserServicesKitTests/PrivacyConfig/PrivacyConfigurationDataTests.swift +++ b/Tests/BrowserServicesKitTests/PrivacyConfig/PrivacyConfigurationDataTests.swift @@ -1,6 +1,5 @@ // // PrivacyConfigurationDataTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/PrivacyConfig/PrivacyConfigurationReferenceTests.swift b/Tests/BrowserServicesKitTests/PrivacyConfig/PrivacyConfigurationReferenceTests.swift index 3cb8b6e57..2eed811a6 100644 --- a/Tests/BrowserServicesKitTests/PrivacyConfig/PrivacyConfigurationReferenceTests.swift +++ b/Tests/BrowserServicesKitTests/PrivacyConfig/PrivacyConfigurationReferenceTests.swift @@ -1,6 +1,5 @@ // // PrivacyConfigurationReferenceTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -27,7 +26,7 @@ final class PrivacyConfigurationReferenceTests: XCTestCase { static let configRootPath = "Resources/privacy-reference-tests/privacy-configuration" static let tests = "Resources/privacy-reference-tests/privacy-configuration/tests.json" } - + func testPrivacyConfiguration() throws { let dataLoader = JsonTestDataLoader() let testsData = dataLoader.fromJsonFile(Resource.tests) @@ -36,10 +35,10 @@ final class PrivacyConfigurationReferenceTests: XCTestCase { for testConfig in referenceTests.testConfigs { let path = "\(Resource.configRootPath)/\(testConfig.referenceConfig)" - + let configData = dataLoader.fromJsonFile(path) let privacyConfigurationData = try PrivacyConfigurationData(data: configData) - + let privacyConfiguration = AppPrivacyConfiguration(data: privacyConfigurationData, identifier: UUID().uuidString, localProtection: MockDomainsProtectionStore(), @@ -49,23 +48,23 @@ final class PrivacyConfigurationReferenceTests: XCTestCase { os_log("Skipping test %@", test.name) continue } - + let testInfo = "\nName: \(test.name)\nFeature: \(test.featureName)\nsiteURL: \(test.siteURL)\nConfig: \(testConfig.referenceConfig)" - + guard let url = URL(string: test.siteURL), let siteDomain = url.host else { XCTFail("Can't get domain \(testInfo)") continue } - + if let feature = PrivacyFeature(rawValue: test.featureName) { let isEnabled = privacyConfiguration.isFeature(feature, enabledForDomain: siteDomain) XCTAssertEqual(isEnabled, test.expectFeatureEnabled, testInfo) - + } else if test.featureName == "trackerAllowlist" { let isEnabled = privacyConfigurationData.trackerAllowlist.state == "enabled" XCTAssertEqual(isEnabled, test.expectFeatureEnabled, testInfo) - + } else { XCTFail("Can't create feature \(testInfo)") continue @@ -78,7 +77,7 @@ final class PrivacyConfigurationReferenceTests: XCTestCase { // MARK: - TestData private struct TestData: Codable { let testConfigs: [TestConfig] - + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let dict = try container.decode([String: TestConfig].self) @@ -90,7 +89,7 @@ private struct TestData: Codable { private struct TestConfig: Codable { let name, desc, referenceConfig: String let tests: [Test] - + } // MARK: - Test diff --git a/Tests/BrowserServicesKitTests/ReferrerTrimming/ReferrerTrimmingTests.swift b/Tests/BrowserServicesKitTests/ReferrerTrimming/ReferrerTrimmingTests.swift index 2868757d8..02fffdf3e 100644 --- a/Tests/BrowserServicesKitTests/ReferrerTrimming/ReferrerTrimmingTests.swift +++ b/Tests/BrowserServicesKitTests/ReferrerTrimming/ReferrerTrimmingTests.swift @@ -32,13 +32,13 @@ struct ReferrerTests: Codable { let expectReferrerHeaderValue: String? let exceptPlatforms: [String]? } - + struct ReferrerHeaderTestSuite: Codable { let name: String let desc: String let tests: [ReferrerHeaderTest] } - + let refererHeaderNavigation: ReferrerHeaderTestSuite } @@ -49,10 +49,10 @@ class ReferrerTrimmingTests: XCTestCase { static let tds = "Resources/privacy-reference-tests/referrer-trimming/tracker_radar_reference.json" static let tests = "Resources/privacy-reference-tests/referrer-trimming/tests.json" } - + private static let data = JsonTestDataLoader() private static let config = data.fromJsonFile(Resource.config) - + private var privacyManager: PrivacyConfigurationManager { let embeddedDataProvider = MockEmbeddedDataProvider(data: Self.config, etag: "embedded") @@ -65,43 +65,43 @@ class ReferrerTrimmingTests: XCTestCase { localProtection: localProtection, internalUserDecider: DefaultInternalUserDecider()) } - + private var contentBlockingManager: ContentBlockerRulesManager { let listsSource = ContentBlockerRulesListSourceMock() let exceptionsSource = ContentBlockerRulesExceptionsSourceMock() return ContentBlockerRulesManager(rulesSource: listsSource, exceptionsSource: exceptionsSource) } - + private lazy var tds: TrackerData = { let trackerJSON = Self.data.fromJsonFile(Resource.tds) return try! JSONDecoder().decode(TrackerData.self, from: trackerJSON) }() - + private lazy var referrerTestSuite: ReferrerTests = { let tests = Self.data.fromJsonFile(Resource.tests) return try! JSONDecoder().decode(ReferrerTests.self, from: tests) }() - + func testReferrerTrimming() throws { let tests = referrerTestSuite.refererHeaderNavigation.tests let referrerTrimming = ReferrerTrimming(privacyManager: privacyManager, contentBlockingManager: contentBlockingManager, tld: TLD()) - + for test in tests { let skip = test.exceptPlatforms?.contains("ios-browser") if skip == true { os_log("!!SKIPPING TEST: %s", test.name) continue } - + os_log("TEST: %s", test.name) - + let referrerResult = referrerTrimming.getTrimmedReferrer(originUrl: URL(string: test.navigatingFromURL)!, destUrl: URL(string: test.navigatingToURL)!, referrerUrl: test.referrerValue != nil ? URL(string: test.referrerValue!) : nil, trackerData: tds) - + // nil result is considered unchanged let resultUrl = referrerResult == nil ? test.referrerValue : referrerResult XCTAssertEqual(resultUrl, test.expectReferrerHeaderValue, "\(test.name) failed") diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift index 91486cd99..6b0a038c3 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Mappers/JsonToRemoteConfigModelMapperTests.swift @@ -1,6 +1,5 @@ // // JsonToRemoteConfigModelMapperTests.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/AppAttributeMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/AppAttributeMatcherTests.swift index a488fe438..909ed8263 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/AppAttributeMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/AppAttributeMatcherTests.swift @@ -1,6 +1,5 @@ // // AppAttributeMatcherTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/DeviceAttributeMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/DeviceAttributeMatcherTests.swift index 00714d1eb..ad6192721 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/DeviceAttributeMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/DeviceAttributeMatcherTests.swift @@ -1,6 +1,5 @@ // // DeviceAttributeMatcherTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift index 04398c249..d8b63f122 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Matchers/UserAttributeMatcherTests.swift @@ -1,6 +1,5 @@ // // UserAttributeMatcherTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/Model/RangeStringMatchingAttributeTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/Model/RangeStringMatchingAttributeTests.swift index 1abdfb856..a79bef798 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/Model/RangeStringMatchingAttributeTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/Model/RangeStringMatchingAttributeTests.swift @@ -1,6 +1,5 @@ // // RangeStringMatchingAttributeTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift index 9d80cc3c2..4b152b088 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigMatcherTests.swift @@ -1,6 +1,5 @@ // // RemoteMessagingConfigMatcherTests.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift index 593e8cf30..24d378591 100644 --- a/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift +++ b/Tests/BrowserServicesKitTests/RemoteMessaging/RemoteMessagingConfigProcessorTests.swift @@ -1,6 +1,5 @@ // -// JsonRemoteMessagingConfigMapperTests.swift -// DuckDuckGo +// RemoteMessagingConfigProcessorTests.swift // // Copyright © 2017 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/SecureVault/CredentialsDatabaseCleanerTests.swift b/Tests/BrowserServicesKitTests/SecureVault/CredentialsDatabaseCleanerTests.swift index d99aa8c4b..5110c9dcc 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/CredentialsDatabaseCleanerTests.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/CredentialsDatabaseCleanerTests.swift @@ -1,6 +1,5 @@ // // CredentialsDatabaseCleanerTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift b/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift index c4dd9956d..62e3b13a0 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/MockAutofillDatabaseProvider.swift @@ -1,5 +1,5 @@ // -// MockProviders.swift +// MockAutofillDatabaseProvider.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -72,7 +72,7 @@ internal class MockAutofillDatabaseProvider: AutofillDatabaseProvider { } func deleteWebsiteCredentialsForAccountId(_ accountId: Int64) throws { - self._credentialsDict.removeValue(forKey: accountId) + self._credentialsDict.removeValue(forKey: accountId) self._accounts = self._accounts.filter { $0.id != String(accountId) } } diff --git a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultManagerTests.swift b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultManagerTests.swift index d5b675fee..5b2679820 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultManagerTests.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultManagerTests.swift @@ -23,13 +23,13 @@ import SecureStorageTestsUtils @testable import BrowserServicesKit class SecureVaultManagerTests: XCTestCase { - + private var mockCryptoProvider = NoOpCryptoProvider() private var mockKeystoreProvider = MockKeystoreProvider() private var mockDatabaseProvider: MockAutofillDatabaseProvider = { return try! MockAutofillDatabaseProvider() }() - + private let mockAutofillUserScript: AutofillUserScript = { let embeddedConfig = """ @@ -49,7 +49,7 @@ class SecureVaultManagerTests: XCTestCase { properties: properties) return AutofillUserScript(scriptSourceProvider: sourceProvider, encrypter: MockEncrypter(), hostProvider: SecurityOriginHostProvider()) }() - + private var testVault: (any AutofillSecureVault)! private var secureVaultManagerDelegate: MockSecureVaultManagerDelegate! private var manager: SecureVaultManager! @@ -61,65 +61,65 @@ class SecureVaultManagerTests: XCTestCase { mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8) let providers = SecureStorageProviders(crypto: mockCryptoProvider, database: mockDatabaseProvider, keystore: mockKeystoreProvider) - + self.testVault = DefaultAutofillSecureVault(providers: providers) self.secureVaultManagerDelegate = MockSecureVaultManagerDelegate() self.manager = SecureVaultManager(vault: self.testVault) self.manager.delegate = secureVaultManagerDelegate } - + func testWhenGettingExistingEntries_AndNoAutofillDataWasProvided_AndNoEntriesExist_ThenReturnValueIsNil() throws { let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: nil, creditCard: nil, trigger: nil) let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData) - + XCTAssertNil(entries.credentials) XCTAssertNil(entries.identity) XCTAssertNil(entries.creditCard) } - + func testWhenGettingExistingEntries_AndAutofillCreditCardWasProvided_AndNoMatchingCreditCardExists_ThenReturnValueIncludesCard() throws { let card = paymentMethod(cardNumber: "5555555555555557", cardholderName: "Name", cvv: "123", month: 1, year: 2022) let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: nil, creditCard: card, trigger: nil) let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData) - + XCTAssertNil(entries.credentials) XCTAssertNil(entries.identity) XCTAssertNotNil(entries.creditCard) XCTAssertTrue(entries.creditCard!.hasAutofillEquality(comparedTo: card)) } - + func testWhenGettingExistingEntries_AndAutofillCreditCardWasProvided_AndMatchingCreditCardExists_ThenReturnValueIsNil() throws { let card = paymentMethod(id: 1, cardNumber: "5555555555555557", cardholderName: "Name", cvv: "123", month: 1, year: 2022) try self.testVault.storeCreditCard(card) let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: nil, creditCard: card, trigger: nil) let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData) - + XCTAssertNil(entries.credentials) XCTAssertNil(entries.identity) XCTAssertNil(entries.creditCard) } - + func testWhenGettingExistingEntries_AndAutofillIdentityWasProvided_AndNoMatchingIdentityExists_ThenReturnValueIncludesIdentity() throws { let identity = identity(name: ("First", "Middle", "Last"), addressStreet: "Address Street") - + let autofillData = AutofillUserScript.DetectedAutofillData(identity: identity, credentials: nil, creditCard: nil, trigger: nil) let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData) - + XCTAssertNil(entries.credentials) XCTAssertNil(entries.creditCard) XCTAssertNotNil(entries.identity) XCTAssertTrue(entries.identity!.hasAutofillEquality(comparedTo: identity)) } - + func testWhenGettingExistingEntries_AndAutofillIdentityWasProvided_AndMatchingIdentityExists_ThenReturnValueIsNil() throws { let identity = identity(id: 1, name: ("First", "Middle", "Last"), addressStreet: "Address Street") try self.testVault.storeIdentity(identity) let autofillData = AutofillUserScript.DetectedAutofillData(identity: identity, credentials: nil, creditCard: nil, trigger: nil) let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData) - + XCTAssertNil(entries.credentials) XCTAssertNil(entries.identity) XCTAssertNil(entries.creditCard) @@ -163,7 +163,7 @@ class SecureVaultManagerTests: XCTestCase { self.secureVaultManagerDelegate = SecureVaultDelegate() self.manager.delegate = self.secureVaultManagerDelegate - + let triggerType = AutofillUserScript.GetTriggerType.userInitiated // account 1 (empty username) @@ -207,7 +207,7 @@ class SecureVaultManagerTests: XCTestCase { self.secureVaultManagerDelegate = SecureVaultDelegate() self.manager.delegate = self.secureVaultManagerDelegate - + let triggerType = AutofillUserScript.GetTriggerType.userInitiated // account 1 (empty username) @@ -383,7 +383,7 @@ class SecureVaultManagerTests: XCTestCase { } - // When generating an email and then changing to personal duck address input, credentials should not be autosaved (prompt should be presented instead) + // When generating an email and then changing to personal duck address input, credentials should not be autosaved (prompt should be presented instead) func testWhenGeneratedUsernameIsChangedToPersonalDuckAddress_ThenDataIsNotAutosaved() { var incomingCredentials = AutofillUserScript.IncomingCredentials(username: "privateemail@duck.com", password: "", autogenerated: true) var incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .emailProtection) @@ -392,9 +392,9 @@ class SecureVaultManagerTests: XCTestCase { // Email should be saved let credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertEqual(credentials?.account.username, "privateemail@duck.com") - XCTAssertEqual(credentials?.password, Data("".utf8)) + XCTAssertEqual(credentials?.password, Data("".utf8)) - // Select Private Email address and submit + // Select Private Email address and submit incomingCredentials = AutofillUserScript.IncomingCredentials(username: "john1@duck.com", password: "", autogenerated: false) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .emailProtection) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) @@ -402,7 +402,7 @@ class SecureVaultManagerTests: XCTestCase { incomingCredentials = AutofillUserScript.IncomingCredentials(username: "john1@duck.com", password: "QNKs6k4a-axYX@aRQW", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) let entries = try? manager.existingEntries(for: "fill.dev", autofillData: incomingData) - + // Confirm autofill entries are present XCTAssertEqual(entries?.credentials?.account.username, "john1@duck.com") XCTAssertEqual(entries?.credentials?.password, Data("QNKs6k4a-axYX@aRQW".utf8)) @@ -416,68 +416,68 @@ class SecureVaultManagerTests: XCTestCase { XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.account.username, "john1@duck.com") XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.password, Data("QNKs6k4a-axYX@aRQW".utf8)) - + } - - // When generating an email and then changing to manual input, credentials should not be autosaved (prompt should be presented instead) + + // When generating an email and then changing to manual input, credentials should not be autosaved (prompt should be presented instead) func testWhenGeneratedUsernameIsChangedToManualInput_ThenDataIsNotAutosaved() { var incomingCredentials = AutofillUserScript.IncomingCredentials(username: "akla11@duck.com", password: "", autogenerated: true) var incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .emailProtection) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // Email should be saved let credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertEqual(credentials?.account.username, "akla11@duck.com") - XCTAssertEqual(credentials?.password, Data("".utf8)) - + XCTAssertEqual(credentials?.password, Data("".utf8)) + // Autofill prompted data tests incomingCredentials = AutofillUserScript.IncomingCredentials(username: "example@duck.com", password: "QNKs6k212aYX@aRQW", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) let entries = try? manager.existingEntries(for: "fill.dev", autofillData: incomingData) XCTAssertEqual(entries?.credentials?.account.username, "example@duck.com") XCTAssertEqual(entries?.credentials?.password, Data("QNKs6k212aYX@aRQW".utf8)) - + incomingCredentials = AutofillUserScript.IncomingCredentials(username: "john1@duck.com", password: "QNKs6k4a-axYX@aRQW", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + let creds = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertNil(creds) - + // Prompted data should be there XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.account.username, "john1@duck.com") XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.password, Data("QNKs6k4a-axYX@aRQW".utf8)) - + } - - // When generating an email and then changing to manual input, credentials should not be autosaved (prompt should be presented instead) + + // When generating an email and then changing to manual input, credentials should not be autosaved (prompt should be presented instead) func testWhenGeneratedUsernameIsManuallyChanged_ThenDataIsNotAutosaved() { var incomingCredentials = AutofillUserScript.IncomingCredentials(username: "privateemail@duck.com", password: "", autogenerated: true) var incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .emailProtection) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // Autofill prompted data tests incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) let entries = try? manager.existingEntries(for: "fill.dev", autofillData: incomingData) XCTAssertEqual(entries?.credentials?.account.username, "email@example.com") XCTAssertEqual(entries?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) - + // Submit the form incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // No data should be saved let credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertNil(credentials) - + // Prompted data should be there XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.account.username, "email@example.com") XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) - + } // When generating and entering a manual password, then deleting the automatically saved login @@ -524,160 +524,159 @@ class SecureVaultManagerTests: XCTestCase { // Create mocked Autofill Data incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: true) - incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) - + incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) + let entries = try? manager.existingEntries(for: "fill.dev", autofillData: incomingData) XCTAssertEqual(entries?.credentials?.account.username, "email@example.com") XCTAssertEqual(entries?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) } - + // When the user generates a pasword and there is a username present from the autofill script, it should be automatically saved too func testWhenGeneratingAPassword_ThenUsernameShouldBeSavedIfPresent() { let incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "gener4tedP4sswOrd", autogenerated: true) let incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .passwordGeneration) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + let credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertEqual(credentials?.account.username, "email@example.com") XCTAssertEqual(credentials?.password, Data("gener4tedP4sswOrd".utf8)) } - + // When submitting a form that never had autogenerated data, a prompt is shown func testWhenEnteringManualUsernameAndPassword_ThenDataIsSaved() { var incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: false) var incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // Create mocked Autofill Data incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: true) - incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) - + incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) + let entries = try? manager.existingEntries(for: "fill.dev", autofillData: incomingData) XCTAssertEqual(entries?.credentials?.account.username, "email@example.com") XCTAssertEqual(entries?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) - + // Prompted data should be there XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.account.username, "email@example.com") XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) } - + // When autosaving credentials for one site, and the using the same username in other site, data should not be automatically saved func testWhenSavingCredentialsAutomatically_PartialAccountShouldBeCleared() { var incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "gener4tedP4sswOrd", autogenerated: true) var incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .passwordGeneration) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "gener4tedP4sswOrd", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // Credentials should be saved automatically var credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertEqual(credentials?.account.username, "email@example.com") XCTAssertEqual(credentials?.password, Data("gener4tedP4sswOrd".utf8)) - + incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: false) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "profile.theguardian.com", data: incomingData) - + // Credentials should NOT saved automatically credentials = try? testVault?.websiteCredentialsFor(accountId: 2) XCTAssertNil(credentials) - + // Create mocked Autofill Data incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: false) - incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) - + incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) + let entries = try? manager.existingEntries(for: "fill.dev", autofillData: incomingData) XCTAssertEqual(entries?.credentials?.account.username, "email@example.com") XCTAssertEqual(entries?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) - + // Prompted data should be there XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.account.username, "email@example.com") XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) } - + // If an account already exists, its data should not be auto-replaced when generating usernames or passwords (on a different session) func testWhenAutosavingCredentialsForAndOldAccount_ThenAccountShouldNotBeUpdatedAutomatically() { - + var incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "gener4tedP4sswOrd", autogenerated: true) var incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .passwordGeneration) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // A form submission should close the existing session incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "gener4tedP4sswOrd", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + var credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertEqual(credentials?.account.username, "email@example.com") XCTAssertEqual(credentials?.password, Data("gener4tedP4sswOrd".utf8)) - + // The user then goes back to the form and auto generates a password incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "Anoth3rgener4tedP4sswOrd", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .passwordGeneration) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // The new password should not be saved credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertEqual(credentials?.account.username, "email@example.com") XCTAssertEqual(credentials?.password, Data("gener4tedP4sswOrd".utf8)) - + // The user then goes back to the form and auto generates a username incomingCredentials = AutofillUserScript.IncomingCredentials(username: "privateemail@duck.com", password: "", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .emailProtection) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // The new password should not be saved credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertEqual(credentials?.account.username, "email@example.com") XCTAssertEqual(credentials?.password, Data("gener4tedP4sswOrd".utf8)) - + } - + // When generating a private email address, and manually typing a password, and typing a manual email // and submitting the form, a prompt to save data should be shown, and no data should be automatically saved func testWhenUsingPrivateAndThenManuallyTypedEmail_ThenDataShouldNotBeAutosaved() { - + // Create a login item via a generated username and manual password var incomingCredentials = AutofillUserScript.IncomingCredentials(username: "privateemail@duck.com", password: "m4nu4lP4sswOrd", autogenerated: true) var incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .emailProtection) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // The new email should not be saved var credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertEqual(credentials?.account.username, "privateemail@duck.com") XCTAssertEqual(credentials?.password, Data("m4nu4lP4sswOrd".utf8)) - - // Change the email to a manual and submit the form + + // Change the email to a manual and submit the form incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: true) incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "fill.dev", data: incomingData) - + // Credentials should NOT saved automatically credentials = try? testVault?.websiteCredentialsFor(accountId: 1) XCTAssertNil(credentials) - + // Create mocked Autofill Data incomingCredentials = AutofillUserScript.IncomingCredentials(username: "email@example.com", password: "m4nu4lP4sswOrd", autogenerated: false) - incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) - + incomingData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil, trigger: .formSubmission) + let entries = try? manager.existingEntries(for: "fill.dev", autofillData: incomingData) XCTAssertEqual(entries?.credentials?.account.username, "email@example.com") XCTAssertEqual(entries?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) - + // Prompted data should be there XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.account.username, "email@example.com") XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.password, Data("m4nu4lP4sswOrd".utf8)) - + } - + // MARK: - Test Utilities - // swiftlint:disable:next large_tuple private func identity(id: Int64? = nil, name: (String, String, String), addressStreet: String?) -> SecureVaultModels.Identity { return SecureVaultModels.Identity(id: id, title: nil, @@ -699,7 +698,7 @@ class SecureVaultManagerTests: XCTestCase { mobilePhone: nil, emailAddress: nil) } - + private func paymentMethod(id: Int64? = nil, cardNumber: String, cardholderName: String, @@ -714,7 +713,7 @@ class SecureVaultManagerTests: XCTestCase { expirationMonth: month, expirationYear: year) } - + } private class MockSecureVaultManagerDelegate: SecureVaultManagerDelegate { @@ -732,7 +731,7 @@ private class MockSecureVaultManagerDelegate: SecureVaultManagerDelegate { } func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, isAuthenticatedFor type: BrowserServicesKit.AutofillType, completionHandler: @escaping (Bool) -> Void) {} - + func secureVaultManager(_: SecureVaultManager, promptUserToAutofillCredentialsForDomain domain: String, withAccounts accounts: [SecureVaultModels.WebsiteAccount], @@ -740,13 +739,13 @@ private class MockSecureVaultManagerDelegate: SecureVaultManagerDelegate { completionHandler: @escaping (SecureVaultModels.WebsiteAccount?) -> Void) {} func secureVaultManager(_: BrowserServicesKit.SecureVaultManager, promptUserWithGeneratedPassword password: String, completionHandler: @escaping (Bool) -> Void) {} - + func secureVaultManager(_: SecureVaultManager, didAutofill type: AutofillType, withObjectId objectId: String) {} - + func secureVaultManager(_: SecureVaultManager, didRequestAuthenticationWithCompletionHandler: @escaping (Bool) -> Void) {} - + func secureVaultInitFailed(_ error: SecureStorageError) {} - + func secureVaultManagerShouldSaveData(_: BrowserServicesKit.SecureVaultManager) -> Bool { true } diff --git a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultModelTests.swift b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultModelTests.swift index 67e8b52df..642aa5596 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultModelTests.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultModelTests.swift @@ -14,6 +14,7 @@ // 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 XCTest @@ -155,7 +156,6 @@ class SecureVaultModelTests: XCTestCase { // MARK: - Test Utilities - // swiftlint:disable:next large_tuple private func identity(named name: (String, String, String), addressStreet: String?) -> SecureVaultModels.Identity { return SecureVaultModels.Identity(id: nil, title: nil, @@ -331,7 +331,7 @@ class SecureVaultModelTests: XCTestCase { } func testPatternMatchedTitle() { - + let domainTitles: [String] = [ "duck.com", "duck.com (test@duck.com)", @@ -347,7 +347,7 @@ class SecureVaultModelTests: XCTestCase { "https://www.duck.com/section/page.php?test=variable1&b=variable2", "https://WwW.dUck.com/section/page" ] - + let subdomainTitles: [String] = [ "signin.duck.com", "signin.duck.com (test@duck.com.co)", @@ -357,7 +357,7 @@ class SecureVaultModelTests: XCTestCase { "https://signin.duck.com/section/page.php?test=variable1&b=variable2", "https://SiGnIn.dUck.com/section/page" ] - + let tldPlusOneTitles: [String] = [ "signin.duck.com.co", "signin.duck.com.co (test@duck.com.co)", @@ -367,7 +367,7 @@ class SecureVaultModelTests: XCTestCase { "https://signin.duck.com.co/section/page.php?test=variable1&b=variable2", "https://SiGnIn.dUck.com.CO/section/page" ] - + let randomTitles: [String] = [ "John's Work Gmail", "Chase Bank - Main Account", @@ -422,36 +422,36 @@ class SecureVaultModelTests: XCTestCase { "twitter.com my account", "fill.dev personal email" ] - + for title in domainTitles { let account = SecureVaultModels.WebsiteAccount(id: "", title: title, username: "", domain: "sometestdomain.com", created: Date(), lastUpdated: Date()) XCTAssertEqual("duck.com", account.patternMatchedTitle(), "Failed for title: \(title)") - + let equalDomain = SecureVaultModels.WebsiteAccount(id: "", title: title, username: "", domain: "duck.com", created: Date(), lastUpdated: Date()) XCTAssertEqual("", equalDomain.patternMatchedTitle(), "Failed for title: \(title)") } - + for title in subdomainTitles { let account = SecureVaultModels.WebsiteAccount(id: "", title: title, username: "", domain: "sometestdomain.com", created: Date(), lastUpdated: Date()) XCTAssertEqual("signin.duck.com", account.patternMatchedTitle(), "Failed for title: \(title)") - + let equalDomain = SecureVaultModels.WebsiteAccount(id: "", title: title, username: "", domain: "signin.duck.com", created: Date(), lastUpdated: Date()) XCTAssertEqual("", equalDomain.patternMatchedTitle(), "Failed for title: \(title)") } - + for title in tldPlusOneTitles { let account = SecureVaultModels.WebsiteAccount(id: "", title: title, username: "", domain: "sometestdomain.com", created: Date(), lastUpdated: Date()) XCTAssertEqual("signin.duck.com.co", account.patternMatchedTitle(), "Failed for title: \(title)") - + let equalDomain = SecureVaultModels.WebsiteAccount(id: "", title: title, username: "", domain: "signin.duck.com.co", created: Date(), lastUpdated: Date()) XCTAssertEqual("", equalDomain.patternMatchedTitle(), "Failed for title: \(title)") } - + for title in randomTitles { let account = SecureVaultModels.WebsiteAccount(id: "", title: title, username: "", domain: "sometestdomain.com", created: Date(), lastUpdated: Date()) XCTAssertEqual(title, account.patternMatchedTitle(), "Failed for title: \(title)") } - + } - + } diff --git a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultSyncableCredentialsMigrationPerformanceTests.swift b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultSyncableCredentialsMigrationPerformanceTests.swift index 3703d10f3..003d6d70d 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultSyncableCredentialsMigrationPerformanceTests.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultSyncableCredentialsMigrationPerformanceTests.swift @@ -1,6 +1,5 @@ // // SecureVaultSyncableCredentialsMigrationPerformanceTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultSyncableCredentialsTests.swift b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultSyncableCredentialsTests.swift index af438860f..26c0a649a 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultSyncableCredentialsTests.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultSyncableCredentialsTests.swift @@ -1,6 +1,5 @@ // // SecureVaultSyncableCredentialsTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultTests.swift b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultTests.swift index 42d77442a..53326a8b7 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultTests.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultTests.swift @@ -80,7 +80,7 @@ class SecureVaultTests: XCTestCase { mockCryptoProvider._derivedKey = "derived".data(using: .utf8)! mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8)! mockCryptoProvider._decryptedData = "decrypted".data(using: .utf8)! - + let account = SecureVaultModels.WebsiteAccount(id: "1", title: "Title", username: "test@duck.com", @@ -88,7 +88,7 @@ class SecureVaultTests: XCTestCase { created: Date(), lastUpdated: Date()) let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: "password".data(using: .utf8)!) - + try testVault.storeWebsiteCredentials(credentials) mockDatabaseProvider._accounts = [account] @@ -166,12 +166,12 @@ class SecureVaultTests: XCTestCase { let account = SecureVaultModels.WebsiteAccount(id: "1", username: "test@duck.com", domain: "example.com", created: Date(), lastUpdated: Date()) let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: password) self.mockDatabaseProvider._accounts = [account] - + mockCryptoProvider._decryptedData = "decrypted".data(using: .utf8) mockKeystoreProvider._generatedPassword = "generated".data(using: .utf8) mockCryptoProvider._derivedKey = "derived".data(using: .utf8) mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8) - + try testVault.storeWebsiteCredentials(credentials) let fetchedCredentials = try testVault.websiteCredentialsFor(accountId: 1) @@ -188,11 +188,11 @@ class SecureVaultTests: XCTestCase { let account = SecureVaultModels.WebsiteAccount(id: "1", username: "test@duck.com", domain: "example.com", created: Date(), lastUpdated: Date()) let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: password) self.mockDatabaseProvider._accounts = [account] - + mockCryptoProvider._decryptedData = "decrypted".data(using: .utf8) mockCryptoProvider._derivedKey = "derived".data(using: .utf8) mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8) - + _ = try testVault.authWith(password: userPassword) try testVault.storeWebsiteCredentials(credentials) @@ -209,7 +209,7 @@ class SecureVaultTests: XCTestCase { mockCryptoProvider._decryptedData = "decrypted".data(using: .utf8) mockCryptoProvider._derivedKey = "derived".data(using: .utf8) mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8) - + _ = try testVault.authWith(password: userPassword) sleep(2) // allow vault to expire password diff --git a/Tests/BrowserServicesKitTests/SmarterEncryption/BloomFilterWrapperTest.swift b/Tests/BrowserServicesKitTests/SmarterEncryption/BloomFilterWrapperTest.swift index 79699b66c..bc058429d 100644 --- a/Tests/BrowserServicesKitTests/SmarterEncryption/BloomFilterWrapperTest.swift +++ b/Tests/BrowserServicesKitTests/SmarterEncryption/BloomFilterWrapperTest.swift @@ -1,6 +1,5 @@ // // BloomFilterWrapperTest.swift -// DuckDuckGo // // Copyright © 2018 DuckDuckGo. All rights reserved. // @@ -23,32 +22,32 @@ import XCTest @testable import BloomFilterWrapper class BloomFilterWrapperTest: XCTestCase { - + struct Constants { static let filterElementCount = 1000 static let additionalTestDataElementCount = 1000 static let targetErrorRate = 0.001 static let acceptableErrorRate = Constants.targetErrorRate * 5 } - + func testWhenBloomFilterEmptyThenContainsIsFalse() { let testee = BloomFilterWrapper(totalItems: Int32(Constants.filterElementCount), errorRate: Constants.targetErrorRate) XCTAssertFalse(testee.contains("abc")) } - + func testWhenBloomFilterContainsElementThenContainsIsTrue() { let testee = BloomFilterWrapper(totalItems: Int32(Constants.filterElementCount), errorRate: Constants.targetErrorRate) testee.add("abc") XCTAssertTrue(testee.contains("abc")) } - + func testWhenBloomFilterContainsItemsThenLookupResultsAreWithinRange() { let bloomData = createRandomStrings(count: Constants.filterElementCount) let testData = bloomData + createRandomStrings(count: Constants.additionalTestDataElementCount) - + let testee = BloomFilterWrapper(totalItems: Int32(bloomData.count), errorRate: Constants.targetErrorRate) bloomData.forEach { testee.add($0) } - + var falsePositives = 0, truePositives = 0, falseNegatives = 0, trueNegatives = 0 for element in testData { let result = testee.contains(element) @@ -57,18 +56,18 @@ class BloomFilterWrapperTest: XCTestCase { if !bloomData.contains(element) && !result { trueNegatives += 1 } if bloomData.contains(element) && result { truePositives += 1 } } - + let errorRate = Double(falsePositives) / Double(testData.count) XCTAssertEqual(0, falseNegatives) XCTAssertEqual(bloomData.count, truePositives) XCTAssertTrue(trueNegatives <= testData.count - bloomData.count) XCTAssertTrue(errorRate <= Constants.acceptableErrorRate) } - + private func createRandomStrings(count: Int) -> [String] { var list = [String]() for _ in 0..(["www.example.com", "example.com", "test.com", "anothertest.com"]), Set(result!)) } - + func testWhenBloomFilterSpecificationJSONIsUnexpectedThenTypeMismatchErrorThrown() { let data = JsonTestDataLoader().unexpected() XCTAssertThrowsError(try HTTPSUpgradeParser.convertBloomFilterSpecification(fromJSONData: data), "") { error in XCTAssertEqual(error.localizedDescription, JsonError.typeMismatch.localizedDescription) } } - + func testWhenBloomFilterSpecificationJSONIsInvalidThenInvalidJsonErrorThrown() { let data = JsonTestDataLoader().invalid() XCTAssertThrowsError(try HTTPSUpgradeParser.convertBloomFilterSpecification(fromJSONData: data)) { error in XCTAssertEqual(error.localizedDescription, JsonError.invalidJson.localizedDescription) } } - + func testWhenBloomFilterSpecificationIsValidThenSpecificationReturned() { let data = JsonTestDataLoader().fromJsonFile("Resources/https_bloom_spec.json") let result = try? HTTPSUpgradeParser.convertBloomFilterSpecification(fromJSONData: data) diff --git a/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeReferenceTests.swift b/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeReferenceTests.swift index 242bea3fa..89641e34c 100644 --- a/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeReferenceTests.swift +++ b/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeReferenceTests.swift @@ -1,6 +1,5 @@ // // HTTPSUpgradeReferenceTests.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // @@ -29,7 +28,7 @@ private struct HTTPSUpgradesRefTests: Decodable { let desc: String let tests: [HTTPSUpgradesTest] } - + struct HTTPSUpgradesTest: Decodable { let name: String let siteURL: String @@ -37,16 +36,16 @@ private struct HTTPSUpgradesRefTests: Decodable { let requestType: String let expectURL: String let exceptPlatforms: [String] - + var shouldSkip: Bool { exceptPlatforms.contains("ios-browser") } } - + let navigations: HTTPSUpgradesTests let subrequests: HTTPSUpgradesTests } final class HTTPSUpgradeReferenceTests: XCTestCase { - + private enum Resource { static let config = "Resources/privacy-reference-tests/https-upgrades/config_reference.json" static let tests = "Resources/privacy-reference-tests/https-upgrades/tests.json" @@ -54,9 +53,9 @@ final class HTTPSUpgradeReferenceTests: XCTestCase { static let bloomFilterSpec = "Resources/privacy-reference-tests/https-upgrades/https_bloomfilter_spec_reference.json" static let bloomFilter = "Resources/privacy-reference-tests/https-upgrades/https_bloomfilter_reference" } - + private static let data = JsonTestDataLoader() - + private static let config = data.fromJsonFile(Resource.config) private static let emptyConfig = """ @@ -68,51 +67,51 @@ final class HTTPSUpgradeReferenceTests: XCTestCase { } } """.data(using: .utf8)! - + private func makePrivacyManager(config: Data? = config, unprotectedDomains: [String] = []) -> PrivacyConfigurationManager { let embeddedDataProvider = MockEmbeddedDataProvider(data: config ?? Self.emptyConfig, etag: "embedded") let localProtection = MockDomainsProtectionStore() localProtection.unprotectedDomains = Set(unprotectedDomains) - + return PrivacyConfigurationManager(fetchedETag: nil, fetchedData: nil, embeddedDataProvider: embeddedDataProvider, localProtection: localProtection, internalUserDecider: DefaultInternalUserDecider()) } - + private lazy var httpsUpgradesTestSuite: HTTPSUpgradesRefTests = { let tests = Self.data.fromJsonFile(Resource.tests) return try! JSONDecoder().decode(HTTPSUpgradesRefTests.self, from: tests) }() - + private lazy var excludedDomains: [String] = { let allowListData = Self.data.fromJsonFile(Resource.allowList) return try! HTTPSUpgradeParser.convertExcludedDomainsData(allowListData) }() - + private lazy var bloomFilterSpecification: HTTPSBloomFilterSpecification = { let data = Self.data.fromJsonFile(Resource.bloomFilterSpec) return try! HTTPSUpgradeParser.convertBloomFilterSpecification(fromJSONData: data) }() - + private lazy var bloomFilter: BloomFilterWrapper? = { let path = Bundle.module.path(forResource: Resource.bloomFilter, ofType: "bin")! return BloomFilterWrapper(fromPath: path, withBitCount: Int32(bloomFilterSpecification.bitCount), andTotalItems: Int32(bloomFilterSpecification.totalEntries)) }() - + private lazy var mockStore: HTTPSUpgradeStore = { HTTPSUpgradeStoreMock(bloomFilter: bloomFilter, bloomFilterSpecification: bloomFilterSpecification, excludedDomains: excludedDomains) }() - + func testHTTPSUpgradesNavigations() async { let tests = httpsUpgradesTestSuite.navigations.tests let httpsUpgrade = HTTPSUpgrade(store: mockStore, privacyManager: makePrivacyManager()) await httpsUpgrade.loadData() - + for test in tests { os_log("TEST: %s", test.name) @@ -120,12 +119,12 @@ final class HTTPSUpgradeReferenceTests: XCTestCase { os_log("SKIPPING TEST: \(test.name)") return } - + guard let url = URL(string: test.requestURL) else { XCTFail("BROKEN INPUT: \(Resource.tests)") return } - + var resultURL = url let result = await httpsUpgrade.upgrade(url: url) if case let .success(upgradedURL) = result { @@ -134,28 +133,28 @@ final class HTTPSUpgradeReferenceTests: XCTestCase { XCTAssertEqual(resultURL.absoluteString, test.expectURL, "FAILED: \(test.name)") } } - + func testLocalUnprotectedDomainShouldNotUpgradeToHTTPS() async { let httpsUpgrade = HTTPSUpgrade(store: mockStore, privacyManager: makePrivacyManager(config: nil, unprotectedDomains: ["secure.thirdtest.com"])) await httpsUpgrade.loadData() - + let url = URL(string: "http://secure.thirdtest.com")! - + var resultURL = url let result = await httpsUpgrade.upgrade(url: url) if case let .success(upgradedURL) = result { resultURL = upgradedURL } - + XCTAssertEqual(resultURL.absoluteString, url.absoluteString, "FAILED: \(resultURL)") } - + func testLocalUnprotectedDomainShouldUpgradeSubdomainToHTTPS() async { let httpsUpgrade = HTTPSUpgrade(store: mockStore, privacyManager: makePrivacyManager(config: nil, unprotectedDomains: ["thirdtest.com"])) await httpsUpgrade.loadData() - + let url = URL(string: "http://secure.thirdtest.com")! - + var resultURL = url let result = await httpsUpgrade.upgrade(url: url) if case let .success(upgradedURL) = result { diff --git a/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeStoreMock.swift b/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeStoreMock.swift index f14339161..43e834b19 100644 --- a/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeStoreMock.swift +++ b/Tests/BrowserServicesKitTests/SmarterEncryption/HTTPSUpgradeStoreMock.swift @@ -1,6 +1,5 @@ // // HTTPSUpgradeStoreMock.swift -// DuckDuckGo // // Copyright © 2022 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/Statistics/MockStatisticsStore.swift b/Tests/BrowserServicesKitTests/Statistics/MockStatisticsStore.swift index aba39aced..729d7d3e0 100644 --- a/Tests/BrowserServicesKitTests/Statistics/MockStatisticsStore.swift +++ b/Tests/BrowserServicesKitTests/Statistics/MockStatisticsStore.swift @@ -1,6 +1,5 @@ // // MockStatisticsStore.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/Statistics/MockVariantManager.swift b/Tests/BrowserServicesKitTests/Statistics/MockVariantManager.swift index 3ed76d171..ba302c936 100644 --- a/Tests/BrowserServicesKitTests/Statistics/MockVariantManager.swift +++ b/Tests/BrowserServicesKitTests/Statistics/MockVariantManager.swift @@ -1,6 +1,5 @@ // // MockVariantManager.swift -// DuckDuckGo // // Copyright © 2018 DuckDuckGo. All rights reserved. // @@ -28,7 +27,7 @@ struct MockVariantManager: VariantManager { isSupportedBlock = { _ in return newValue } } } - + var isSupportedBlock: (FeatureName) -> Bool var currentVariant: Variant? @@ -41,7 +40,7 @@ struct MockVariantManager: VariantManager { func assignVariantIfNeeded(_ newInstallCompletion: (VariantManager) -> Void) { } - + func isSupported(feature: FeatureName) -> Bool { return isSupportedBlock(feature) } diff --git a/Tests/BrowserServicesKitTests/Suggestions/ScoreTests.swift b/Tests/BrowserServicesKitTests/Suggestions/ScoreTests.swift index b9d013dad..0a10c12a2 100644 --- a/Tests/BrowserServicesKitTests/Suggestions/ScoreTests.swift +++ b/Tests/BrowserServicesKitTests/Suggestions/ScoreTests.swift @@ -95,5 +95,5 @@ final class ScoreTests: XCTestCase { XCTAssert(score1 < score2) } - + } diff --git a/Tests/BrowserServicesKitTests/Utils/JsonTestDataLoader.swift b/Tests/BrowserServicesKitTests/Utils/JsonTestDataLoader.swift index 05df51d26..89f10b726 100644 --- a/Tests/BrowserServicesKitTests/Utils/JsonTestDataLoader.swift +++ b/Tests/BrowserServicesKitTests/Utils/JsonTestDataLoader.swift @@ -1,6 +1,5 @@ // // JsonTestDataLoader.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // @@ -27,18 +26,18 @@ enum FileError: Error { final class FileLoader { func load(filePath: String, fromBundle bundle: Bundle) throws -> Data { - + guard let resourceUrl = bundle.resourceURL else { throw FileError.unknownFile } - + let url = resourceUrl.appendingPathComponent(filePath) - + let finalURL: URL if FileManager.default.fileExists(atPath: url.path) { finalURL = url } else { // Workaround for resource bundle having a different structure when running tests from command line. let url = resourceUrl.deletingLastPathComponent().appendingPathComponent(filePath) - + if FileManager.default.fileExists(atPath: url.path) { finalURL = url } else { diff --git a/Tests/BrowserServicesKitTests/Utils/StringExtension.swift b/Tests/BrowserServicesKitTests/Utils/StringExtension.swift index d15912f60..40463f0a3 100644 --- a/Tests/BrowserServicesKitTests/Utils/StringExtension.swift +++ b/Tests/BrowserServicesKitTests/Utils/StringExtension.swift @@ -1,6 +1,5 @@ // // StringExtension.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/BrowserServicesKitTests/XCTestManifests.swift b/Tests/BrowserServicesKitTests/XCTestManifests.swift index 3c1ee008d..b03615fab 100644 --- a/Tests/BrowserServicesKitTests/XCTestManifests.swift +++ b/Tests/BrowserServicesKitTests/XCTestManifests.swift @@ -1,3 +1,21 @@ +// +// XCTestManifests.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + import XCTest #if !canImport(ObjectiveC) diff --git a/Tests/CommonTests/AppVersionExtensionTests.swift b/Tests/CommonTests/AppVersionExtensionTests.swift index dbd5bb4c2..b4c13820f 100644 --- a/Tests/CommonTests/AppVersionExtensionTests.swift +++ b/Tests/CommonTests/AppVersionExtensionTests.swift @@ -1,6 +1,5 @@ // // AppVersionExtensionTests.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // @@ -33,7 +32,7 @@ final class AppVersionExtensionTests: XCTestCase { override func setUp() { super.setUp() - + mockBundle = MockBundle() testee = AppVersion(bundle: mockBundle) } @@ -44,7 +43,7 @@ final class AppVersionExtensionTests: XCTestCase { mockBundle.add(name: Bundle.Key.buildNumber, value: Constants.build) XCTAssertEqual("2.0.4.14", testee.versionAndBuildNumber) } - + func testLocalisedTextContainsNameVersionAndBuild() { mockBundle.add(name: Bundle.Key.name, value: Constants.name) mockBundle.add(name: Bundle.Key.versionNumber, value: Constants.version) diff --git a/Tests/CommonTests/AppVersionTests.swift b/Tests/CommonTests/AppVersionTests.swift index ca8209495..8fa66d69a 100644 --- a/Tests/CommonTests/AppVersionTests.swift +++ b/Tests/CommonTests/AppVersionTests.swift @@ -1,6 +1,5 @@ // // AppVersionTests.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // @@ -34,7 +33,7 @@ final class AppVersionTests: XCTestCase { override func setUp() { super.setUp() - + mockBundle = MockBundle() testee = AppVersion(bundle: mockBundle) } @@ -48,7 +47,7 @@ final class AppVersionTests: XCTestCase { mockBundle.add(name: Bundle.Key.versionNumber, value: Constants.version) XCTAssertEqual("2", testee.majorVersionNumber) } - + func testVersionNumber() { mockBundle.add(name: Bundle.Key.versionNumber, value: Constants.version) XCTAssertEqual(Constants.version, testee.versionNumber) diff --git a/Tests/CommonTests/Concurrency/TaskTimeoutTests.swift b/Tests/CommonTests/Concurrency/TaskTimeoutTests.swift index 8a5778f6e..0ddb153ef 100644 --- a/Tests/CommonTests/Concurrency/TaskTimeoutTests.swift +++ b/Tests/CommonTests/Concurrency/TaskTimeoutTests.swift @@ -1,6 +1,5 @@ // // TaskTimeoutTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -35,7 +34,7 @@ final class TaskTimeoutTests: XCTestCase { XCTAssertEqual(result, 1) } - + func testWithTimeoutThrowsTimeoutError() async { do { try await withTimeout(0.0001) { diff --git a/Tests/CommonTests/Extensions/StringExtensionTests.swift b/Tests/CommonTests/Extensions/StringExtensionTests.swift index 4b005b71e..cc341fdfa 100644 --- a/Tests/CommonTests/Extensions/StringExtensionTests.swift +++ b/Tests/CommonTests/Extensions/StringExtensionTests.swift @@ -25,47 +25,47 @@ final class StringExtensionTests: XCTestCase { func testWhenNormalizingStringsForAutofill_ThenDiacriticsAreRemoved() { let stringToNormalize = "Dáx Thê Dûck" let normalizedString = stringToNormalize.autofillNormalized() - + XCTAssertEqual(normalizedString, "daxtheduck") } - + func testWhenNormalizingStringsForAutofill_ThenWhitespaceIsRemoved() { let stringToNormalize = "Dax The Duck" let normalizedString = stringToNormalize.autofillNormalized() - + XCTAssertEqual(normalizedString, "daxtheduck") } - + func testWhenNormalizingStringsForAutofill_ThenPunctuationIsRemoved() { let stringToNormalize = ",Dax+The_Duck." let normalizedString = stringToNormalize.autofillNormalized() - + XCTAssertEqual(normalizedString, "daxtheduck") } - + func testWhenNormalizingStringsForAutofill_ThenNumbersAreRetained() { let stringToNormalize = "Dax123" let normalizedString = stringToNormalize.autofillNormalized() - + XCTAssertEqual(normalizedString, "dax123") } - + func testWhenNormalizingStringsForAutofill_ThenStringsThatDoNotNeedNormalizationAreUntouched() { let stringToNormalize = "firstmiddlelast" let normalizedString = stringToNormalize.autofillNormalized() - + XCTAssertEqual(normalizedString, "firstmiddlelast") } - + func testWhenNormalizingStringsForAutofill_ThenEmojiAreRemoved() { let stringToNormalize = "Dax 🤔" let normalizedString = stringToNormalize.autofillNormalized() - + XCTAssertEqual(normalizedString, "dax") } - + func testWhenEmojisArePresentInDomains_ThenTheseCanBePunycoded() { - + XCTAssertEqual("example.com".punycodeEncodedHostname, "example.com") XCTAssertEqual("Dax🤔.com".punycodeEncodedHostname, "xn--dax-v153b.com") XCTAssertEqual("🤔.com".punycodeEncodedHostname, "xn--wp9h.com") diff --git a/Tests/CommonTests/Mocks/MockBundle.swift b/Tests/CommonTests/Mocks/MockBundle.swift index bdb784d1e..23aed843f 100644 --- a/Tests/CommonTests/Mocks/MockBundle.swift +++ b/Tests/CommonTests/Mocks/MockBundle.swift @@ -1,6 +1,5 @@ // // MockBundle.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // diff --git a/Tests/CommonTests/TLD/TLDTests.swift b/Tests/CommonTests/TLD/TLDTests.swift index 8860a2362..9e2d78960 100644 --- a/Tests/CommonTests/TLD/TLDTests.swift +++ b/Tests/CommonTests/TLD/TLDTests.swift @@ -1,6 +1,5 @@ // // TLDTests.swift -// DuckDuckGo // // Copyright © 2017 DuckDuckGo. All rights reserved. // @@ -46,11 +45,11 @@ final class TLDTests: XCTestCase { func testWhenHostIsTopLevelDotComThenDomainIsSame() { XCTAssertEqual("example.com", tld.domain("example.com")) } - + func testWhenHostIsMalformedThenDomainIsFixed() { XCTAssertEqual("example.com", tld.domain(".example.com")) } - + func testWhenHostMultiPartTopLevelWithSubdomainThenETLDp1Correct() { XCTAssertEqual("bbc.co.uk", tld.eTLDplus1("www.bbc.co.uk")) XCTAssertEqual("bbc.co.uk", tld.eTLDplus1("other.bbc.co.uk")) @@ -67,25 +66,25 @@ final class TLDTests: XCTestCase { XCTAssertEqual(nil, tld.eTLDplus1("com")) XCTAssertEqual(nil, tld.eTLDplus1("co.uk")) } - + func testWhenHostIsIncorrectThenETLDp1IsNotFound() { XCTAssertEqual(nil, tld.eTLDplus1("abcderfg")) } - + func testWhenHostIsNilDomainIsNil() { XCTAssertNil(tld.domain(nil)) } - + func testWhenHostIsTLDThenDomainIsFound() { XCTAssertEqual("com", tld.domain("com")) XCTAssertEqual("co.uk", tld.domain("co.uk")) } - + func testWhenHostIsMultiPartTLDThenDomainIsFound() { XCTAssertEqual(nil, tld.domain("za")) XCTAssertEqual("co.za", tld.domain("co.za")) } - + func testWhenHostIsIncorrectThenDomainIsNil() { XCTAssertNil(tld.domain("abcdefgh")) } diff --git a/Tests/ConfigurationTests/ConfigurationFetcherTests.swift b/Tests/ConfigurationTests/ConfigurationFetcherTests.swift index 77449768e..7fbfe1aae 100644 --- a/Tests/ConfigurationTests/ConfigurationFetcherTests.swift +++ b/Tests/ConfigurationTests/ConfigurationFetcherTests.swift @@ -1,6 +1,5 @@ // // ConfigurationFetcherTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -23,16 +22,16 @@ import XCTest @testable import TestUtils final class ConfigurationFetcherTests: XCTestCase { - + enum MockError: Error { case someError } - + override class func setUp() { APIRequest.Headers.setUserAgent("") Configuration.setURLProvider(MockConfigurationURLProvider()) } - + func makeConfigurationFetcher(store: ConfigurationStoring = MockStore(), validator: ConfigurationValidating = MockValidator()) -> ConfigurationFetcher { let testConfiguration = URLSessionConfiguration.default @@ -41,7 +40,7 @@ final class ConfigurationFetcherTests: XCTestCase { validator: validator, urlSession: URLSession(configuration: testConfiguration)) } - + let privacyConfigurationData = Data("Privacy Config".utf8) // MARK: - Tests for fetch(_:) @@ -55,11 +54,11 @@ final class ConfigurationFetcherTests: XCTestCase { let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(.privacyConfiguration) - + XCTAssertEqual(store.loadData(for: .privacyConfiguration), self.privacyConfigurationData) XCTAssertEqual(store.loadEtag(for: .privacyConfiguration), HTTPURLResponse.testEtag) } - + func testFetchConfigurationWhenNoEtagIsStoredThenResponseIsStored() async throws { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let store = MockStore() @@ -68,18 +67,18 @@ final class ConfigurationFetcherTests: XCTestCase { XCTAssertEqual(store.loadData(for: .privacyConfiguration), self.privacyConfigurationData) XCTAssertEqual(store.loadEtag(for: .privacyConfiguration), HTTPURLResponse.testEtag) } - + func testFetchConfigurationWhenEtagIsStoredButStoreHasNoDataThenResponseIsStored() async throws { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (HTTPURLResponse.testEtag, nil) - + let fetcher = makeConfigurationFetcher(store: store) try await fetcher.fetch(.privacyConfiguration) - + XCTAssertNotNil(store.loadData(for: .privacyConfiguration)) } - + func testFetchConfigurationWhenStoringDataFailsThenEtagIsNotStored() async { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let store = MockStore() @@ -91,76 +90,76 @@ final class ConfigurationFetcherTests: XCTestCase { XCTAssertNil(store.loadData(for: .privacyConfiguration)) XCTAssertNil(store.loadEtag(for: .privacyConfiguration)) } - + func testFetchConfigurationWhenStoringEtagFailsThenEtagIsNotStored() async { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let store = MockStore() store.defaultSaveEtag = { _, _ in throw MockError.someError } - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(.privacyConfiguration) - + XCTAssertNil(store.loadEtag(for: .privacyConfiguration)) } - + func testFetchConfigurationWhenResponseIsNotModifiedThenNoDataStored() async { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.notModified, nil) } let store = MockStore() - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(.privacyConfiguration) - + XCTAssertNil(store.loadEtag(for: .privacyConfiguration)) XCTAssertNil(store.loadData(for: .privacyConfiguration)) } - + func testFetchConfigurationWhenEtagAndDataStoredThenEtagAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let etag = UUID().uuidString let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (etag, Data()) - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(.privacyConfiguration) - + XCTAssertEqual(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch), etag) } - + func testFetchConfigurationWhenNoEtagStoredThenNoEtagAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (nil, Data()) - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(.privacyConfiguration) - + XCTAssertNil(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch)) } - + func testFetchConfigurationWhenNoDataStoredThenNoEtagAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let etag = UUID().uuidString let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (etag, nil) - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(.privacyConfiguration) - + XCTAssertNil(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch)) } - + func testFetchConfigurationWhenEtagProvidedThenItIsAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let etag = UUID().uuidString let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (etag, Data()) - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(.privacyConfiguration) - + XCTAssertEqual(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch), etag) } - + func testFetchConfigurationWhenEmbeddedEtagAndExternalEtagProvidedThenExternalAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let etag = UUID().uuidString @@ -168,15 +167,15 @@ final class ConfigurationFetcherTests: XCTestCase { let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (etag, Data()) store.configToEmbeddedEtag[.privacyConfiguration] = embeddedEtag - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(.privacyConfiguration) - + XCTAssertEqual(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch), etag) } - + // MARK: - Tests for fetch(all:) - + func testFetchAllWhenOneAssetFailsToFetchThenOtherIsNotStoredAndErrorIsThrown() async { MockURLProtocol.requestHandler = { request in if let url = request.url, url == Configuration.bloomFilterBinary.url { @@ -200,13 +199,13 @@ final class ConfigurationFetcherTests: XCTestCase { XCTAssertNil(store.loadData(for: .bloomFilterBinary)) XCTAssertNil(store.loadData(for: .bloomFilterSpec)) } - + func testFetchAllWhenOneAssetFailsToValidateThenOtherIsNotStoredAndErrorIsThrown() async { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let validatorMock = MockValidator() validatorMock.shouldThrowErrorPerConfiguration[.privacyConfiguration] = true validatorMock.shouldThrowErrorPerConfiguration[.trackerDataSet] = false - + let store = MockStore() let fetcher = makeConfigurationFetcher(store: store, validator: validatorMock) do { @@ -223,7 +222,7 @@ final class ConfigurationFetcherTests: XCTestCase { XCTAssertNil(store.loadData(for: .privacyConfiguration)) XCTAssertNil(store.loadData(for: .trackerDataSet)) } - + func testFetchAllWhenEtagAndDataAreStoredThenResponseIsStored() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.ok, self.privacyConfigurationData) } let oldEtag = UUID().uuidString @@ -233,11 +232,11 @@ final class ConfigurationFetcherTests: XCTestCase { let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertEqual(store.loadData(for: .privacyConfiguration), self.privacyConfigurationData) XCTAssertEqual(store.loadEtag(for: .privacyConfiguration), HTTPURLResponse.testEtag) } - + func testFetchAllWhenNoEtagIsStoredThenResponseIsStored() async throws { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let store = MockStore() @@ -246,18 +245,18 @@ final class ConfigurationFetcherTests: XCTestCase { XCTAssertEqual(store.loadData(for: .privacyConfiguration), self.privacyConfigurationData) XCTAssertEqual(store.loadEtag(for: .privacyConfiguration), HTTPURLResponse.testEtag) } - + func testFetchAllWhenEtagIsStoredButStoreHasNoDataThenResponseIsStored() async throws { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (HTTPURLResponse.testEtag, nil) - + let fetcher = makeConfigurationFetcher(store: store) try await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertNotNil(store.loadData(for: .privacyConfiguration)) } - + func testFetchAllWhenStoringDataFailsThenEtagIsNotStored() async { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let store = MockStore() @@ -269,76 +268,76 @@ final class ConfigurationFetcherTests: XCTestCase { XCTAssertNil(store.loadData(for: .privacyConfiguration)) XCTAssertNil(store.loadEtag(for: .privacyConfiguration)) } - + func testFetchAllWhenStoringEtagFailsThenEtagIsNotStored() async { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.ok, self.privacyConfigurationData) } let store = MockStore() store.defaultSaveEtag = { _, _ in throw MockError.someError } - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertNil(store.loadEtag(for: .privacyConfiguration)) } - + func testFetchAllWhenResponseIsNotModifiedThenNoDataStored() async { MockURLProtocol.requestHandler = { _ in ( HTTPURLResponse.notModified, nil) } let store = MockStore() - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertNil(store.loadEtag(for: .privacyConfiguration)) XCTAssertNil(store.loadData(for: .privacyConfiguration)) } - + func testFetchAllWhenEtagAndDataStoredThenEtagAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let etag = UUID().uuidString let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (etag, Data()) - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertEqual(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch), etag) } - + func testFetchAllWhenNoEtagStoredThenNoEtagAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (nil, Data()) - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertNil(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch)) } - + func testFetchAllWhenNoDataStoredThenNoEtagAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let etag = UUID().uuidString let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (etag, nil) - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertNil(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch)) } - + func testFetchAllWhenEtagProvidedThenItIsAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let etag = UUID().uuidString let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (etag, Data()) - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertEqual(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch), etag) } - + func testFetchAllWhenEmbeddedEtagAndExternalEtagProvidedThenExternalAddedToRequest() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let etag = UUID().uuidString @@ -346,11 +345,11 @@ final class ConfigurationFetcherTests: XCTestCase { let store = MockStore() store.configToStoredEtagAndData[.privacyConfiguration] = (etag, Data()) store.configToEmbeddedEtag[.privacyConfiguration] = embeddedEtag - + let fetcher = makeConfigurationFetcher(store: store) try? await fetcher.fetch(all: [.privacyConfiguration]) - + XCTAssertEqual(MockURLProtocol.lastRequest?.value(forHTTPHeaderField: APIRequest.HTTPHeaderField.ifNoneMatch), etag) } - + } diff --git a/Tests/ConfigurationTests/Mocks/MockConfigurationURLProvider.swift b/Tests/ConfigurationTests/Mocks/MockConfigurationURLProvider.swift index 84efe1ee4..460a4a52a 100644 --- a/Tests/ConfigurationTests/Mocks/MockConfigurationURLProvider.swift +++ b/Tests/ConfigurationTests/Mocks/MockConfigurationURLProvider.swift @@ -1,6 +1,5 @@ // // MockConfigurationURLProvider.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,7 +20,7 @@ import Foundation import Configuration struct MockConfigurationURLProvider: ConfigurationURLProviding { - + func url(for configuration: Configuration) -> URL { switch configuration { case .bloomFilterBinary: @@ -40,5 +39,5 @@ struct MockConfigurationURLProvider: ConfigurationURLProviding { return URL(string: "g")! } } - + } diff --git a/Tests/ConfigurationTests/Mocks/MockStore.swift b/Tests/ConfigurationTests/Mocks/MockStore.swift index ef63bacc8..da1f9b4cf 100644 --- a/Tests/ConfigurationTests/Mocks/MockStore.swift +++ b/Tests/ConfigurationTests/Mocks/MockStore.swift @@ -1,6 +1,5 @@ // // MockStore.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -21,34 +20,34 @@ import Foundation @testable import Configuration final class MockStore: ConfigurationStoring { - + var configToEmbeddedEtag = [Configuration: String?]() var configToStoredEtagAndData = [Configuration: (etag: String?, data: Data?)]() var defaultSaveData: ((_ data: Data, _ configuration: Configuration) throws -> Void)? var defaultSaveEtag: ((_ etag: String, _ configuration: Configuration) throws -> Void)? - + init() { defaultSaveData = { data, configuration in let (currentEtag, _) = self.configToStoredEtagAndData[configuration] ?? (nil, nil) self.configToStoredEtagAndData[configuration] = (currentEtag, data) } - + defaultSaveEtag = { etag, configuration in let (_, currentData) = self.configToStoredEtagAndData[configuration] ?? (nil, nil) self.configToStoredEtagAndData[configuration] = (etag, currentData) } } - + func loadData(for configuration: Configuration) -> Data? { configToStoredEtagAndData[configuration]?.data } func loadEtag(for configuration: Configuration) -> String? { configToStoredEtagAndData[configuration]?.etag } func loadEmbeddedEtag(for configuration: Configuration) -> String? { configToEmbeddedEtag[configuration] ?? nil } - + func saveData(_ data: Data, for configuration: Configuration) throws { try defaultSaveData?(data, configuration) } - + func saveEtag(_ etag: String, for configuration: Configuration) throws { try defaultSaveEtag?(etag, configuration) } - + } diff --git a/Tests/ConfigurationTests/Mocks/MockValidator.swift b/Tests/ConfigurationTests/Mocks/MockValidator.swift index 064ebd2af..400215061 100644 --- a/Tests/ConfigurationTests/Mocks/MockValidator.swift +++ b/Tests/ConfigurationTests/Mocks/MockValidator.swift @@ -1,6 +1,5 @@ // // MockValidator.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -29,5 +28,5 @@ final class MockValidator: ConfigurationValidating { throw ConfigurationFetcher.Error.invalidPayload } } - + } diff --git a/Tests/DDGSyncTests/CrypterTests.swift b/Tests/DDGSyncTests/CrypterTests.swift index bfea21663..58619c91f 100644 --- a/Tests/DDGSyncTests/CrypterTests.swift +++ b/Tests/DDGSyncTests/CrypterTests.swift @@ -28,32 +28,32 @@ class CrypterTests: XCTestCase { let crypter = Crypter(secureStore: storage) let userId = "Simple User Name" - + let account = try crypter.createAccountCreationKeys(userId: userId, password: "password") let recoveryKey = SyncCode.RecoveryKey(userId: userId, primaryKey: account.primaryKey) let login = try crypter.extractLoginInfo(recoveryKey: recoveryKey) XCTAssertEqual(account.passwordHash, login.passwordHash) // The login flow calls the server to retreve the protected secret key, but we already have it so check we can decrypt it. - + let secretKey = try crypter.extractSecretKey(protectedSecretKey: account.protectedSecretKey, stretchedPrimaryKey: login.stretchedPrimaryKey) XCTAssertEqual(account.secretKey, secretKey) } - + func testWhenGivenRecoveryKeyThenCanExtractUserIdAndPrimaryKey() throws { let storage = SecureStorageStub() let crypter = Crypter(secureStore: storage) - + let userId = "Simple User Name" let primaryKey = Data([UInt8](repeating: 1, count: Int(DDGSYNCCRYPTO_PRIMARY_KEY_SIZE.rawValue))) - + let recoveryKey = SyncCode.RecoveryKey(userId: userId, primaryKey: primaryKey) let loginInfo = try crypter.extractLoginInfo(recoveryKey: recoveryKey) - + XCTAssertEqual(loginInfo.userId, userId) XCTAssertEqual(loginInfo.primaryKey, primaryKey) } - + func testWhenDecryptingNoneBase64ThenErrorIsThrown() throws { let storage = SecureStorageStub() let primaryKey = Data([UInt8]((0 ..< DDGSYNCCRYPTO_PRIMARY_KEY_SIZE.rawValue).map { _ in UInt8.random(in: 0 ..< UInt8.max )})) diff --git a/Tests/DDGSyncTests/DDGSyncLifecycleTests.swift b/Tests/DDGSyncTests/DDGSyncLifecycleTests.swift index 88690cb25..55066e886 100644 --- a/Tests/DDGSyncTests/DDGSyncLifecycleTests.swift +++ b/Tests/DDGSyncTests/DDGSyncLifecycleTests.swift @@ -1,6 +1,5 @@ // // DDGSyncLifecycleTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/DDGSyncTests/DDGSyncTests.swift b/Tests/DDGSyncTests/DDGSyncTests.swift index 0e17aa2af..3044d7f79 100644 --- a/Tests/DDGSyncTests/DDGSyncTests.swift +++ b/Tests/DDGSyncTests/DDGSyncTests.swift @@ -1,6 +1,5 @@ // // DDGSyncTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/DDGSyncTests/Mocks/Mocks.swift b/Tests/DDGSyncTests/Mocks/Mocks.swift index c2c3e62b5..d0f8d290e 100644 --- a/Tests/DDGSyncTests/Mocks/Mocks.swift +++ b/Tests/DDGSyncTests/Mocks/Mocks.swift @@ -1,6 +1,5 @@ // // Mocks.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/DDGSyncTests/SecureStorageStub.swift b/Tests/DDGSyncTests/SecureStorageStub.swift index 12afb4f9e..52da9b605 100644 --- a/Tests/DDGSyncTests/SecureStorageStub.swift +++ b/Tests/DDGSyncTests/SecureStorageStub.swift @@ -20,12 +20,12 @@ import Foundation @testable import DDGSync class SecureStorageStub: SecureStoring { - + var theAccount: SyncAccount? var mockReadError: SyncError? var mockWriteError: SyncError? - + func persistAccount(_ account: SyncAccount) throws { if let mockWriteError { throw mockWriteError @@ -44,5 +44,5 @@ class SecureStorageStub: SecureStoring { func removeAccount() throws { theAccount = nil } - + } diff --git a/Tests/DDGSyncTests/SyncOperationTests.swift b/Tests/DDGSyncTests/SyncOperationTests.swift index 37a94ac4e..724435157 100644 --- a/Tests/DDGSyncTests/SyncOperationTests.swift +++ b/Tests/DDGSyncTests/SyncOperationTests.swift @@ -1,6 +1,5 @@ // // SyncOperationTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 3c94d4e24..49350f0e6 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,3 +1,21 @@ +// +// LinuxMain.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + import XCTest import BrowserServicesKitTests diff --git a/Tests/NavigationTests/ClosureNavigationResponderTests.swift b/Tests/NavigationTests/ClosureNavigationResponderTests.swift index f33ac280e..50ae712a4 100644 --- a/Tests/NavigationTests/ClosureNavigationResponderTests.swift +++ b/Tests/NavigationTests/ClosureNavigationResponderTests.swift @@ -1,5 +1,5 @@ // -// NavigationValuesTests.swift +// ClosureNavigationResponderTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/NavigationTests/DistributedNavigationDelegateTests.swift b/Tests/NavigationTests/DistributedNavigationDelegateTests.swift index 3375ff14e..449d1f6b3 100644 --- a/Tests/NavigationTests/DistributedNavigationDelegateTests.swift +++ b/Tests/NavigationTests/DistributedNavigationDelegateTests.swift @@ -24,8 +24,6 @@ import XCTest @testable import Navigation // swiftlint:disable unused_closure_parameter -// swiftlint:disable trailing_comma -// swiftlint:disable opening_brace @available(macOS 12.0, iOS 15.0, *) class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase { @@ -51,18 +49,18 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase XCTAssertEqual(nav.state, .finished) eDidFinish.fulfill() } - + server.middleware = [{ [data] request in return .ok(.data(data.html)) }] - + // regular navigation from an empty state try server.start(8084) withWebView { webView in _=webView.load(req(urls.local)) } waitForExpectations(timeout: 5) - + XCTAssertFalse(navAct(1).navigationAction.isTargetingNewWindow) assertHistory(ofResponderAt: 0, equalsTo: [ .navigationAction(req(urls.local), .other, src: main()), @@ -73,7 +71,7 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase .didFinish(Nav(action: navAct(1), .finished, resp: resp(0), .committed)) ]) } - + func testWhenResponderCancelsNavigationAction_followingRespondersNotCalled() { navigationDelegate.setResponders( .strong(NavigationResponderMock(defaultHandler: { _ in })), @@ -92,9 +90,9 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase withWebView { webView in _=webView.load(req(urls.local1)) } - + waitForExpectations(timeout: 5) - + assertHistory(ofResponderAt: 0, equalsTo: [ .navigationAction(req(urls.local1), .other, src: main()), .didCancel(navAct(1)) @@ -104,14 +102,14 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase .didCancel(navAct(1)) ]) } - + func testWhenResponderCancelsNavigationResponse_followingRespondersNotCalled() throws { navigationDelegate.setResponders( .strong(NavigationResponderMock(defaultHandler: { _ in })), .strong(NavigationResponderMock(defaultHandler: { _ in })), .strong(NavigationResponderMock(defaultHandler: { _ in })) ) - + responder(at: 0).onNavigationResponse = { resp in XCTAssertEqual(resp.isSuccessful, false) XCTAssertEqual(resp.httpResponse?.statusCode, 404) @@ -120,7 +118,7 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase } responder(at: 1).onNavigationResponse = { _ in .cancel } responder(at: 2).onNavigationResponse = { _ in XCTFail("Unexpected decidePolicyForNavigationAction:"); return .next } - + let eDidFail = expectation(description: "onDidFail") responder(at: 2).onDidFail = { @MainActor [urls] nav, error in XCTAssertEqual(error._nsError.domain, WKError.WebKitErrorDomain) @@ -129,13 +127,13 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase XCTAssertEqual(error.failingUrl?.matches(urls.local1), true) eDidFail.fulfill() } - + try server.start(8084) withWebView { webView in _=webView.load(req(urls.local1)) } waitForExpectations(timeout: 5) - + assertHistory(ofResponderAt: 0, equalsTo: [ .navigationAction(req(urls.local1), .other, src: main()), .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), @@ -151,18 +149,18 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase .didFail(Nav(action: navAct(1), .failed(WKError(.frameLoadInterruptedByPolicyChange)), resp: resp(0)), WKError.Code.frameLoadInterruptedByPolicyChange.rawValue) ]) } - + func testWhenNavigationFails_didFailIsCalled() { navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in }))) let eDidFail = expectation(description: "onDidFail") responder(at: 0).onDidFail = { _, _ in eDidFail.fulfill() } - + // not calling server.start withWebView { webView in _=webView.load(req(urls.local)) } waitForExpectations(timeout: 5) - + assertHistory(ofResponderAt: 0, equalsTo: [ .navigationAction(req(urls.local), .other, src: main()), .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), @@ -170,14 +168,14 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase .didFail( Nav(action: navAct(1), .failed(WKError(NSURLErrorCannotConnectToHost))), NSURLErrorCannotConnectToHost) ]) } - + func testWhenNavigationActionIsAllowed_followingRespondersNotCalled() throws { navigationDelegate.setResponders( .strong(NavigationResponderMock(defaultHandler: { _ in })), .strong(NavigationResponderMock(defaultHandler: { _ in })), .strong(NavigationResponderMock(defaultHandler: { _ in })) ) - + // Regular navigation without redirects // 1st: .next let eOnNavigationAction1 = expectation(description: "onNavigationAction 1") @@ -187,20 +185,20 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase responder(at: 1).onNavigationAction = { _, _ in eOnNavigationAction2.fulfill(); return .allow } // 3rd: not called responder(at: 2).onNavigationAction = { _, _ in XCTFail("Unexpected navAction"); return .cancel } - + let eDidFinish = expectation(description: "onDidFinish") responder(at: 2).onDidFinish = { _ in eDidFinish.fulfill() } - + server.middleware = [{ [data] request in return .ok(.data(data.html)) }] - + try server.start(8084) withWebView { webView in _=webView.load(req(urls.local)) } waitForExpectations(timeout: 5) - + assertHistory(ofResponderAt: 0, equalsTo: [ .navigationAction(req(urls.local), .other, src: main()), .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), @@ -218,30 +216,30 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase .didFinish(Nav(action: navAct(1), .finished, resp: resp(0), .committed)) ]) } - + func testWhenNavigationResponseAllowed_followingRespondersNotCalled() throws { navigationDelegate.setResponders( .strong(NavigationResponderMock(defaultHandler: { _ in })), .strong(NavigationResponderMock(defaultHandler: { _ in })), .strong(NavigationResponderMock(defaultHandler: { _ in })) ) - + responder(at: 1).onNavigationResponse = { _ in return .allow } responder(at: 2).onNavigationResponse = { _ in XCTFail("Unexpected decidePolicyForNavigationAction:"); return .next } - + let eDidFinish = expectation(description: "onDidFinish") responder(at: 2).onDidFinish = { _ in eDidFinish.fulfill() } - + server.middleware = [{ [data] request in return .ok(.data(data.html)) }] - + try server.start(8084) withWebView { webView in _=webView.load(req(urls.local)) } waitForExpectations(timeout: 5) - + assertHistory(ofResponderAt: 0, equalsTo: [ .navigationAction(req(urls.local), .other, src: main()), .willStart(Nav(action: navAct(1), .approved, isCurrent: false)), @@ -1663,5 +1661,4 @@ class DistributedNavigationDelegateTests: DistributedNavigationDelegateTestsBase } // swiftlint:enable unused_closure_parameter -// swiftlint:enable trailing_comma // swiftlint:enable opening_brace diff --git a/Tests/NavigationTests/Helpers/DistributedNavigationDelegateTestsHelpers.swift b/Tests/NavigationTests/Helpers/DistributedNavigationDelegateTestsHelpers.swift index d1142d729..48d7a15d2 100644 --- a/Tests/NavigationTests/Helpers/DistributedNavigationDelegateTestsHelpers.swift +++ b/Tests/NavigationTests/Helpers/DistributedNavigationDelegateTestsHelpers.swift @@ -69,7 +69,7 @@ class DistributedNavigationDelegateTestsBase: XCTestCase { self.usedDelegates.append(navigationDelegateProxy) navigationDelegateProxy = DistributedNavigationDelegateTests.makeNavigationDelegateProxy() } - + } @available(macOS 12.0, iOS 15.0, *) diff --git a/Tests/NavigationTests/Helpers/NavigationResponderMock.swift b/Tests/NavigationTests/Helpers/NavigationResponderMock.swift index db7650594..c4945e886 100644 --- a/Tests/NavigationTests/Helpers/NavigationResponderMock.swift +++ b/Tests/NavigationTests/Helpers/NavigationResponderMock.swift @@ -181,7 +181,7 @@ class NavigationResponderMock: NavigationResponder { func reset() { clear() - + onNavigationAction = nil onWillStart = nil onDidStart = nil @@ -199,7 +199,7 @@ class NavigationResponderMock: NavigationResponder { onNavResponseWillBecomeDownload = nil onNavResponseBecameDownload = nil - defaultHandler = { + defaultHandler = { fatalError("event received after test completed: \($0)") } } diff --git a/Tests/NavigationTests/Helpers/NavigationTestHelpers.swift b/Tests/NavigationTests/Helpers/NavigationTestHelpers.swift index ce28ab16b..034996702 100644 --- a/Tests/NavigationTests/Helpers/NavigationTestHelpers.swift +++ b/Tests/NavigationTests/Helpers/NavigationTestHelpers.swift @@ -36,7 +36,7 @@ extension TestsNavigationEvent { static func navigationAction(_ request: URLRequest, _ navigationType: NavigationType, from currentHistoryItemIdentity: HistoryItemIdentity? = nil, redirects: [NavAction]? = nil, _ isUserInitiated: NavigationAction.UserInitiated? = nil, src: FrameInfo, _ shouldDownload: NavigationAction.ShouldDownload? = nil, line: UInt = #line) -> TestsNavigationEvent { .navigationAction(.init(request, navigationType, from: currentHistoryItemIdentity, redirects: redirects, isUserInitiated, src: src, targ: src, shouldDownload), line: line) } - + static func response(_ nav: Nav, line: UInt = #line) -> TestsNavigationEvent { .navigationResponse(.navigation(nav), line: line) } diff --git a/Tests/NavigationTests/Helpers/TestNavigationSchemeHandler.swift b/Tests/NavigationTests/Helpers/TestNavigationSchemeHandler.swift index cfab8c7de..9293af19b 100644 --- a/Tests/NavigationTests/Helpers/TestNavigationSchemeHandler.swift +++ b/Tests/NavigationTests/Helpers/TestNavigationSchemeHandler.swift @@ -1,6 +1,5 @@ // // TestNavigationSchemeHandler.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/NavigationTests/NavigationRedirectsTests.swift b/Tests/NavigationTests/NavigationRedirectsTests.swift index a76570812..0e6c173b0 100644 --- a/Tests/NavigationTests/NavigationRedirectsTests.swift +++ b/Tests/NavigationTests/NavigationRedirectsTests.swift @@ -1034,7 +1034,7 @@ class NavigationRedirectsTests: DistributedNavigationDelegateTestsBase { assertHistory(ofResponderAt: 0, equalsTo: [ .navigationAction(NavAction(req(urls.local), .other, from: history[2], src: main(urls.local2))), .didCancel(navAct(3), expected: 2), - + // .navigationAction(NavAction(req(urls.local4, defaultHeaders.allowingExtraKeys), .redirect(.developer), from: history[2], src: main(urls.local2))), // .willStart(Nav(action: navAct(4), redirects: [navAct(3)], .approved, isCurrent: false)), // .didFail(Nav(action: NavAction(req(urls.local4, defaultHeaders.allowingExtraKeys), .redirect(.developer), from: history[2], src: main(urls.local2)), redirects: [navAct(3)], .failed(WKError(NSURLErrorCancelled)), isCurrent: false), NSURLErrorCancelled), diff --git a/Tests/NavigationTests/NavigationValuesTests.swift b/Tests/NavigationTests/NavigationValuesTests.swift index bd11a6df4..b973f27bf 100644 --- a/Tests/NavigationTests/NavigationValuesTests.swift +++ b/Tests/NavigationTests/NavigationValuesTests.swift @@ -113,7 +113,7 @@ class NavigationValuesTests: DistributedNavigationDelegateTestsBase { @MainActor func testNavigationTypes() { navigationDelegate.setResponders(.strong(NavigationResponderMock(defaultHandler: { _ in }))) - + let webView = withWebView { $0 } var navAction = WKNavigationActionMock(sourceFrame: .mock(for: webView, isMain: false), targetFrame: nil, navigationType: .formSubmitted, request: req(urls.local)).navigationAction var e = expectation(description: "decisionHandler 1 called") diff --git a/Tests/NetworkProtectionTests/EndpointTests.swift b/Tests/NetworkProtectionTests/EndpointTests.swift index b732948c8..5d22a405c 100644 --- a/Tests/NetworkProtectionTests/EndpointTests.swift +++ b/Tests/NetworkProtectionTests/EndpointTests.swift @@ -1,6 +1,5 @@ // -// File.swift -// DuckDuckGo +// EndpointTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/NetworkProtectionTests/Mocks/NetworkProtectionServerMocks.swift b/Tests/NetworkProtectionTests/Mocks/NetworkProtectionServerMocks.swift index 4971045f7..a01043439 100644 --- a/Tests/NetworkProtectionTests/Mocks/NetworkProtectionServerMocks.swift +++ b/Tests/NetworkProtectionTests/Mocks/NetworkProtectionServerMocks.swift @@ -32,7 +32,7 @@ extension NetworkProtectionServerInfo { static let mock = NetworkProtectionServerInfo(name: "Mock Server", publicKey: "ovn9RpzUuvQ4XLQt6B3RKuEXGIxa5QpTnehjduZlcSE=", hostNames: ["duckduckgo.com"], - ips: ["192.168.1.1"], + ips: ["192.168.1.1"], internalIP: "10.11.12.1", port: 443, attributes: .init(city: "City", country: "Country", state: "State", timezoneOffset: 0)) diff --git a/Tests/NetworkProtectionTests/NWConnectionExtensionTests.swift b/Tests/NetworkProtectionTests/NWConnectionExtensionTests.swift index f2d6f1698..c2113af73 100644 --- a/Tests/NetworkProtectionTests/NWConnectionExtensionTests.swift +++ b/Tests/NetworkProtectionTests/NWConnectionExtensionTests.swift @@ -1,6 +1,5 @@ // // NWConnectionExtensionTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/NetworkProtectionTests/Notifications/DistributedNotificationObjectCodersTests.swift b/Tests/NetworkProtectionTests/Notifications/DistributedNotificationObjectCodersTests.swift index b271123da..90533910b 100644 --- a/Tests/NetworkProtectionTests/Notifications/DistributedNotificationObjectCodersTests.swift +++ b/Tests/NetworkProtectionTests/Notifications/DistributedNotificationObjectCodersTests.swift @@ -1,5 +1,5 @@ // -// DistributedNotificationObjectCoders.swift +// DistributedNotificationObjectCodersTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/NetworkProtectionTests/Repositories/NetworkProtectionLocationListCompositeRepositoryTests.swift b/Tests/NetworkProtectionTests/Repositories/NetworkProtectionLocationListCompositeRepositoryTests.swift index 71a392d26..87960edd7 100644 --- a/Tests/NetworkProtectionTests/Repositories/NetworkProtectionLocationListCompositeRepositoryTests.swift +++ b/Tests/NetworkProtectionTests/Repositories/NetworkProtectionLocationListCompositeRepositoryTests.swift @@ -1,6 +1,5 @@ // // NetworkProtectionLocationListCompositeRepositoryTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -41,7 +40,7 @@ class NetworkProtectionLocationListCompositeRepositoryTests: XCTestCase { }) } - @MainActor + @MainActor override func tearDown() { NetworkProtectionLocationListCompositeRepository.clearCache() client = nil @@ -143,12 +142,12 @@ class NetworkProtectionLocationListCompositeRepositoryTests: XCTestCase { private extension NetworkProtectionLocation { static func testData(country: String = "", cities: [City] = []) -> NetworkProtectionLocation { - return Self.init(country: country, cities: cities) + return Self(country: country, cities: cities) } } private extension NetworkProtectionLocation.City { static func testData(name: String = "") -> NetworkProtectionLocation.City { - Self.init(name: name) + Self(name: name) } } diff --git a/Tests/NetworkProtectionTests/StartupOptionTests.swift b/Tests/NetworkProtectionTests/StartupOptionTests.swift index 1ade73fb2..e516de5b8 100644 --- a/Tests/NetworkProtectionTests/StartupOptionTests.swift +++ b/Tests/NetworkProtectionTests/StartupOptionTests.swift @@ -1,5 +1,5 @@ // -// StartupOptionsTests.swift +// StartupOptionTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/NetworkProtectionTests/XCTestCase+TemporaryFileURL.swift b/Tests/NetworkProtectionTests/XCTestCase+TemporaryFileURL.swift index c343b11af..1fa81c6bc 100644 --- a/Tests/NetworkProtectionTests/XCTestCase+TemporaryFileURL.swift +++ b/Tests/NetworkProtectionTests/XCTestCase+TemporaryFileURL.swift @@ -1,5 +1,5 @@ // -// TestUtilities.swift +// XCTestCase+TemporaryFileURL.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/NetworkingTests/APIRequestTests.swift b/Tests/NetworkingTests/APIRequestTests.swift index e9c0ef88f..21de657d5 100644 --- a/Tests/NetworkingTests/APIRequestTests.swift +++ b/Tests/NetworkingTests/APIRequestTests.swift @@ -1,6 +1,5 @@ // // APIRequestTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // @@ -22,21 +21,21 @@ import XCTest @testable import TestUtils final class APIRequestTests: XCTestCase { - + enum MockError: Error { case someError } - + override class func setUp() { APIRequest.Headers.setUserAgent("") } - + private var mockURLSession: URLSession { let testConfiguration = URLSessionConfiguration.default testConfiguration.protocolClasses = [MockURLProtocol.self] return URLSession(configuration: testConfiguration) } - + func testWhenUrlSessionThrowsErrorThenWrappedUrlSessionErrorIsThrown() async { MockURLProtocol.requestHandler = { _ in throw MockError.someError } let configuration = APIRequest.Configuration(url: HTTPURLResponse.testUrl) @@ -52,7 +51,7 @@ final class APIRequestTests: XCTestCase { } } } - + func testWhenThereIsNoDataInResponseThenEmptyDataIsReturned() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.ok, nil) } let configuration = APIRequest.Configuration(url: HTTPURLResponse.testUrl) @@ -64,7 +63,7 @@ final class APIRequestTests: XCTestCase { XCTFail("Unexpected error thrown: \(error).") } } - + func testWhenThereIsNoDataInResponseButItIsRequiredThenEmptyDataErrorIsThrown() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.ok, nil) } let configuration = APIRequest.Configuration(url: HTTPURLResponse.testUrl) @@ -79,9 +78,9 @@ final class APIRequestTests: XCTestCase { } } } - + let privacyConfigurationData = Data("Privacy Config".utf8) - + func testWhenEtagIsMissingInResponseThenResponseIsReturned() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.okNoEtag, self.privacyConfigurationData) } let configuration = APIRequest.Configuration(url: HTTPURLResponse.testUrl) @@ -94,7 +93,7 @@ final class APIRequestTests: XCTestCase { XCTFail("Unexpected error thrown: \(error).") } } - + func testWhenEtagIsMissingInResponseButItIsRequiredThenMissingEtagErrorIsThrown() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.okNoEtag, self.privacyConfigurationData) } let configuration = APIRequest.Configuration(url: HTTPURLResponse.testUrl) @@ -109,7 +108,7 @@ final class APIRequestTests: XCTestCase { } } } - + func testWhenInternalServerErrorThenInvalidStatusCodeErrorIsThrown() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.internalServerError, nil) } let configuration = APIRequest.Configuration(url: HTTPURLResponse.testUrl) @@ -124,7 +123,7 @@ final class APIRequestTests: XCTestCase { } } } - + func testWhenNotModifiedResponseThenInvalidResponseErrorIsThrown() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.notModified, nil) } let configuration = APIRequest.Configuration(url: HTTPURLResponse.testUrl) @@ -139,7 +138,7 @@ final class APIRequestTests: XCTestCase { } } } - + func testWhenNotModifiedResponseButItIsAllowedThenResponseWithNilDataIsReturned() async { MockURLProtocol.requestHandler = { _ in (HTTPURLResponse.notModified, nil) } let configuration = APIRequest.Configuration(url: HTTPURLResponse.testUrl) @@ -152,5 +151,5 @@ final class APIRequestTests: XCTestCase { XCTFail("Unexpected error thrown: \(error).") } } - + } diff --git a/Tests/PersistenceTests/CoreDataErrorsParserTests.swift b/Tests/PersistenceTests/CoreDataErrorsParserTests.swift index 01b42245e..ce06dd146 100644 --- a/Tests/PersistenceTests/CoreDataErrorsParserTests.swift +++ b/Tests/PersistenceTests/CoreDataErrorsParserTests.swift @@ -1,6 +1,6 @@ // // CoreDataErrorsParserTests.swift -// +// // Copyright © 2022 DuckDuckGo. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,29 +22,29 @@ import Persistence @objc(TestEntity) class TestEntity: NSManagedObject { - + static let name = "TestEntity" - + public class func entity(in context: NSManagedObjectContext) -> NSEntityDescription { return NSEntityDescription.entity(forEntityName: "TestEntity", in: context)! } - + @NSManaged public var attribute: String? @NSManaged public var relationTo: TestEntity? @NSManaged public var relationFrom: TestEntity? } class CoreDataErrorsParserTests: XCTestCase { - + func tempDBDir() -> URL { FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) } - + var db: CoreDataDatabase! - + func testModel() -> NSManagedObjectModel { let model = NSManagedObjectModel() - + let entity = NSEntityDescription() entity.name = "TestEntity" entity.managedObjectClassName = TestEntity.name @@ -56,10 +56,10 @@ class CoreDataErrorsParserTests: XCTestCase { attribute.attributeType = .stringAttributeType attribute.isOptional = false properties.append(attribute) - + let relationTo = NSRelationshipDescription() let relationFrom = NSRelationshipDescription() - + relationTo.name = "relationTo" relationFrom.isOptional = false relationTo.destinationEntity = entity @@ -67,7 +67,7 @@ class CoreDataErrorsParserTests: XCTestCase { relationTo.maxCount = 1 relationTo.deleteRule = .nullifyDeleteRule relationTo.inverseRelationship = relationFrom - + relationFrom.name = "relationFrom" relationFrom.isOptional = false relationFrom.destinationEntity = entity @@ -75,7 +75,7 @@ class CoreDataErrorsParserTests: XCTestCase { relationFrom.maxCount = 1 relationFrom.deleteRule = .nullifyDeleteRule relationFrom.inverseRelationship = relationTo - + properties.append(relationTo) properties.append(relationFrom) @@ -83,83 +83,83 @@ class CoreDataErrorsParserTests: XCTestCase { model.entities = [entity] return model } - + override func setUp() { super.setUp() - + db = CoreDataDatabase(name: "Test", containerLocation: tempDBDir(), model: testModel()) db.loadStore() } - + override func tearDown() async throws { - + try db.tearDown(deleteStores: true) try await super.tearDown() } - + func testWhenObjectsAreValidThenTheyAreSaved() throws { - + let context = db.makeContext(concurrencyType: .mainQueueConcurrencyType) - + let e1 = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) let e2 = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) - + e1.attribute = "e1" e2.attribute = "e2" e1.relationTo = e2 e2.relationTo = e1 - + try context.save() } - + func testWhenOneAttributesAreMissingThenErrorIsIdentified() { - + let context = db.makeContext(concurrencyType: .mainQueueConcurrencyType) - + let e1 = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) let e2 = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) - + e2.attribute = "e2" e1.relationTo = e2 e2.relationTo = e1 - + do { try context.save() XCTFail("This must fail") } catch { let error = error as NSError - + let info = CoreDataErrorsParser.parse(error: error) XCTAssertEqual(info.first?.entity, TestEntity.name) XCTAssertEqual(info.first?.property, "attribute") } } - + func testWhenMoreAttributesAreMissingThenErrorIsIdentified() { - + let context = db.makeContext(concurrencyType: .mainQueueConcurrencyType) - + _ = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) _ = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) - + do { try context.save() XCTFail("This must fail") } catch { let error = error as NSError - + let info = CoreDataErrorsParser.parse(error: error) XCTAssertEqual(info.count, 4) - + let uniqueSet = Set(info.map { $0.property }) XCTAssertEqual(uniqueSet, ["attribute", "relationFrom"]) } } - + func testWhenStoreIsReadOnlyThenErrorIsIdentified() { - + guard let url = db.coordinator.persistentStores.first?.url else { XCTFail("Failed to get persistent store URL") return @@ -172,58 +172,58 @@ class CoreDataErrorsParserTests: XCTestCase { XCTAssertNil(error) } let context = ro.makeContext(concurrencyType: .mainQueueConcurrencyType) - + let e1 = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) let e2 = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) - + e1.attribute = "e1" e2.attribute = "e2" e1.relationTo = e2 e2.relationTo = e1 - + do { try context.save() XCTFail("This must fail") } catch { let error = error as NSError - + let info = CoreDataErrorsParser.parse(error: error) XCTAssertEqual(info.first?.domain, NSCocoaErrorDomain) XCTAssertEqual(info.first?.code, 513) } } - + func testWhenThereIsMergeConflictThenErrorIsIdentified() throws { - + let context = db.makeContext(concurrencyType: .mainQueueConcurrencyType) - + let e1 = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) let e2 = TestEntity(entity: TestEntity.entity(in: context), insertInto: context) - + e1.attribute = "e1" e2.attribute = "e2" e1.relationTo = e2 e2.relationTo = e1 - + try context.save() - + let anotherContext = db.makeContext(concurrencyType: .mainQueueConcurrencyType) guard let anotherE1 = try anotherContext.existingObject(with: e1.objectID) as? TestEntity else { XCTFail("Expected object") return } - + e1.attribute = "e1updated" try context.save() - + anotherE1.attribute = "e1ConflictingUpdate" - + do { try anotherContext.save() XCTFail("This must fail") } catch { let error = error as NSError - + let info = CoreDataErrorsParser.parse(error: error) XCTAssertEqual(info.first?.domain, NSCocoaErrorDomain) XCTAssertEqual(info.first?.entity, TestEntity.name) diff --git a/Tests/SecureStorageTests/GRDBSecureStorageDatabaseProviderTests.swift b/Tests/SecureStorageTests/GRDBSecureStorageDatabaseProviderTests.swift index 23b6c7afc..709607220 100644 --- a/Tests/SecureStorageTests/GRDBSecureStorageDatabaseProviderTests.swift +++ b/Tests/SecureStorageTests/GRDBSecureStorageDatabaseProviderTests.swift @@ -1,6 +1,5 @@ // // GRDBSecureStorageDatabaseProviderTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SecureStorageTests/SecureStorageCryptoProviderTests.swift b/Tests/SecureStorageTests/SecureStorageCryptoProviderTests.swift index d04947591..ff1135fbe 100644 --- a/Tests/SecureStorageTests/SecureStorageCryptoProviderTests.swift +++ b/Tests/SecureStorageTests/SecureStorageCryptoProviderTests.swift @@ -1,6 +1,5 @@ // // SecureStorageCryptoProviderTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SecureStorageTests/SecureVaultFactoryTests.swift b/Tests/SecureStorageTests/SecureVaultFactoryTests.swift index 16200eb45..03c00aa5f 100644 --- a/Tests/SecureStorageTests/SecureVaultFactoryTests.swift +++ b/Tests/SecureStorageTests/SecureVaultFactoryTests.swift @@ -1,5 +1,5 @@ // -// MockVault.swift +// SecureVaultFactoryTests.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SecureStorageTests/TestMocks.swift b/Tests/SecureStorageTests/TestMocks.swift index 3869b3c9c..88f187fc6 100644 --- a/Tests/SecureStorageTests/TestMocks.swift +++ b/Tests/SecureStorageTests/TestMocks.swift @@ -1,6 +1,5 @@ // -// MockProviders.swift -// DuckDuckGo +// TestMocks.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksInitialSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksInitialSyncResponseHandlerTests.swift index bf33e3fc6..a07339303 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksInitialSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksInitialSyncResponseHandlerTests.swift @@ -1,6 +1,5 @@ // // BookmarksInitialSyncResponseHandlerTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift index 82c80bd3a..ceb3f2f0c 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksProviderTests.swift @@ -1,6 +1,5 @@ // // BookmarksProviderTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift index 752af2fb4..9cf55fc04 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/BookmarksRegularSyncResponseHandlerTests.swift @@ -1,6 +1,5 @@ // // BookmarksRegularSyncResponseHandlerTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Bookmarks/SyncableBookmarkAdapterTests.swift b/Tests/SyncDataProvidersTests/Bookmarks/SyncableBookmarkAdapterTests.swift index 741b808ab..2ad67bd52 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/SyncableBookmarkAdapterTests.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/SyncableBookmarkAdapterTests.swift @@ -1,6 +1,5 @@ // // SyncableBookmarkAdapterTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Bookmarks/helpers/BookmarksProviderTestsBase.swift b/Tests/SyncDataProvidersTests/Bookmarks/helpers/BookmarksProviderTestsBase.swift index 9b2c5ccf5..08f535486 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/helpers/BookmarksProviderTestsBase.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/helpers/BookmarksProviderTestsBase.swift @@ -1,6 +1,5 @@ // // BookmarksProviderTestsBase.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Bookmarks/helpers/SyncableBookmarksExtension.swift b/Tests/SyncDataProvidersTests/Bookmarks/helpers/SyncableBookmarksExtension.swift index bf30700f4..172be7401 100644 --- a/Tests/SyncDataProvidersTests/Bookmarks/helpers/SyncableBookmarksExtension.swift +++ b/Tests/SyncDataProvidersTests/Bookmarks/helpers/SyncableBookmarksExtension.swift @@ -1,6 +1,5 @@ // // SyncableBookmarksExtension.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Credentials/CredentialsInitialSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Credentials/CredentialsInitialSyncResponseHandlerTests.swift index 09b463653..5d64af593 100644 --- a/Tests/SyncDataProvidersTests/Credentials/CredentialsInitialSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Credentials/CredentialsInitialSyncResponseHandlerTests.swift @@ -1,6 +1,5 @@ // // CredentialsInitialSyncResponseHandlerTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Credentials/CredentialsProviderTests.swift b/Tests/SyncDataProvidersTests/Credentials/CredentialsProviderTests.swift index 8a84055f2..1b98b7bd3 100644 --- a/Tests/SyncDataProvidersTests/Credentials/CredentialsProviderTests.swift +++ b/Tests/SyncDataProvidersTests/Credentials/CredentialsProviderTests.swift @@ -1,6 +1,5 @@ // // CredentialsProviderTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Credentials/CredentialsRegularSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Credentials/CredentialsRegularSyncResponseHandlerTests.swift index 0f5271a31..a5a41367a 100644 --- a/Tests/SyncDataProvidersTests/Credentials/CredentialsRegularSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Credentials/CredentialsRegularSyncResponseHandlerTests.swift @@ -1,6 +1,5 @@ // // CredentialsRegularSyncResponseHandlerTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Credentials/helpers/CredentialsProviderTestsBase.swift b/Tests/SyncDataProvidersTests/Credentials/helpers/CredentialsProviderTestsBase.swift index 93ec6dec3..d58676d1a 100644 --- a/Tests/SyncDataProvidersTests/Credentials/helpers/CredentialsProviderTestsBase.swift +++ b/Tests/SyncDataProvidersTests/Credentials/helpers/CredentialsProviderTestsBase.swift @@ -1,6 +1,5 @@ // // CredentialsProviderTestsBase.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Credentials/helpers/SyncableCredentialsExtension.swift b/Tests/SyncDataProvidersTests/Credentials/helpers/SyncableCredentialsExtension.swift index ab15e5540..a18f95df5 100644 --- a/Tests/SyncDataProvidersTests/Credentials/helpers/SyncableCredentialsExtension.swift +++ b/Tests/SyncDataProvidersTests/Credentials/helpers/SyncableCredentialsExtension.swift @@ -1,6 +1,5 @@ // // SyncableCredentialsExtension.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Credentials/helpers/TestAutofillSecureVaultFactory.swift b/Tests/SyncDataProvidersTests/Credentials/helpers/TestAutofillSecureVaultFactory.swift index 9fd29ab75..0b33e3439 100644 --- a/Tests/SyncDataProvidersTests/Credentials/helpers/TestAutofillSecureVaultFactory.swift +++ b/Tests/SyncDataProvidersTests/Credentials/helpers/TestAutofillSecureVaultFactory.swift @@ -1,6 +1,5 @@ // -// TestSecureVaultFactory.swift -// DuckDuckGo +// TestAutofillSecureVaultFactory.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/CryptingMock.swift b/Tests/SyncDataProvidersTests/CryptingMock.swift index 32810c95e..a5ab06128 100644 --- a/Tests/SyncDataProvidersTests/CryptingMock.swift +++ b/Tests/SyncDataProvidersTests/CryptingMock.swift @@ -1,6 +1,5 @@ // // CryptingMock.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Settings/SettingsInitialSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Settings/SettingsInitialSyncResponseHandlerTests.swift index 7ef3b9007..95546c68f 100644 --- a/Tests/SyncDataProvidersTests/Settings/SettingsInitialSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Settings/SettingsInitialSyncResponseHandlerTests.swift @@ -1,6 +1,5 @@ // // SettingsInitialSyncResponseHandlerTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Settings/SettingsProviderTests.swift b/Tests/SyncDataProvidersTests/Settings/SettingsProviderTests.swift index 902726e38..f35c1f720 100644 --- a/Tests/SyncDataProvidersTests/Settings/SettingsProviderTests.swift +++ b/Tests/SyncDataProvidersTests/Settings/SettingsProviderTests.swift @@ -1,6 +1,5 @@ // // SettingsProviderTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Settings/SettingsRegularSyncResponseHandlerTests.swift b/Tests/SyncDataProvidersTests/Settings/SettingsRegularSyncResponseHandlerTests.swift index 8d6219617..cb7619a65 100644 --- a/Tests/SyncDataProvidersTests/Settings/SettingsRegularSyncResponseHandlerTests.swift +++ b/Tests/SyncDataProvidersTests/Settings/SettingsRegularSyncResponseHandlerTests.swift @@ -1,6 +1,5 @@ // // SettingsRegularSyncResponseHandlerTests.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Settings/helpers/SettingsProviderTestsBase.swift b/Tests/SyncDataProvidersTests/Settings/helpers/SettingsProviderTestsBase.swift index 081e6e494..ef15a1ef2 100644 --- a/Tests/SyncDataProvidersTests/Settings/helpers/SettingsProviderTestsBase.swift +++ b/Tests/SyncDataProvidersTests/Settings/helpers/SettingsProviderTestsBase.swift @@ -1,6 +1,5 @@ // // SettingsProviderTestsBase.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Settings/helpers/SyncableSettingsExtension.swift b/Tests/SyncDataProvidersTests/Settings/helpers/SyncableSettingsExtension.swift index 57efdae4b..09b49a851 100644 --- a/Tests/SyncDataProvidersTests/Settings/helpers/SyncableSettingsExtension.swift +++ b/Tests/SyncDataProvidersTests/Settings/helpers/SyncableSettingsExtension.swift @@ -1,6 +1,5 @@ // // SyncableSettingsExtension.swift -// DuckDuckGo // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/SyncDataProvidersTests/Settings/helpers/TestSettingSyncHandler.swift b/Tests/SyncDataProvidersTests/Settings/helpers/TestSettingSyncHandler.swift index bc3457fb5..8735bc12d 100644 --- a/Tests/SyncDataProvidersTests/Settings/helpers/TestSettingSyncHandler.swift +++ b/Tests/SyncDataProvidersTests/Settings/helpers/TestSettingSyncHandler.swift @@ -1,6 +1,5 @@ // -// TestSettingHandler.swift -// DuckDuckGo +// TestSettingSyncHandler.swift // // Copyright © 2023 DuckDuckGo. All rights reserved. // diff --git a/Tests/UserScriptTests/StaticUserScriptTests.swift b/Tests/UserScriptTests/StaticUserScriptTests.swift index b065dd496..580280108 100644 --- a/Tests/UserScriptTests/StaticUserScriptTests.swift +++ b/Tests/UserScriptTests/StaticUserScriptTests.swift @@ -1,6 +1,5 @@ // // StaticUserScriptTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/UserScriptTests/UserScriptEncrypterTests.swift b/Tests/UserScriptTests/UserScriptEncrypterTests.swift index dc7058c72..7ea993faf 100644 --- a/Tests/UserScriptTests/UserScriptEncrypterTests.swift +++ b/Tests/UserScriptTests/UserScriptEncrypterTests.swift @@ -1,6 +1,5 @@ // // UserScriptEncrypterTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // diff --git a/Tests/UserScriptTests/UserScriptMessagingTests.swift b/Tests/UserScriptTests/UserScriptMessagingTests.swift index 60ef4868b..3bed9c37b 100644 --- a/Tests/UserScriptTests/UserScriptMessagingTests.swift +++ b/Tests/UserScriptTests/UserScriptMessagingTests.swift @@ -1,6 +1,5 @@ // -// ContentScopeMessagingTests.swift -// DuckDuckGo +// UserScriptMessagingTests.swift // // Copyright © 2021 DuckDuckGo. All rights reserved. // @@ -60,7 +59,7 @@ class UserScriptMessagingTests: XCTestCase { } } catch {} - wait(for: [expectation], timeout: 2.0) + await fulfillment(of: [expectation], timeout: 2.0) } /// This test verifies that the replyHandler is called for notifications, @@ -90,7 +89,7 @@ class UserScriptMessagingTests: XCTestCase { expectation.fulfill() } catch {} - wait(for: [expectation], timeout: 2.0) + await fulfillment(of: [expectation], timeout: 2.0) } /// This test verifies that errors from handlers are reflected back to the JS side. @@ -128,7 +127,7 @@ class UserScriptMessagingTests: XCTestCase { } } catch {} - wait(for: [expectation], timeout: 2.0) + await fulfillment(of: [expectation], timeout: 2.0) } /// Ensure that an error is thrown if the feature was not registered @@ -188,8 +187,6 @@ class UserScriptMessagingTests: XCTestCase { } } -// swiftlint:disable large_tuple - /// A helper for registering a test delegate and creating a MockMsg based on the /// incoming dictionary (which represents a message coming from a webview) /// diff --git a/Tests/UserScriptTests/UserScriptTests.swift b/Tests/UserScriptTests/UserScriptTests.swift index 873509155..9751fac2b 100644 --- a/Tests/UserScriptTests/UserScriptTests.swift +++ b/Tests/UserScriptTests/UserScriptTests.swift @@ -1,6 +1,5 @@ // // UserScriptTests.swift -// DuckDuckGo // // Copyright © 2021 DuckDuckGo. All rights reserved. // From ae9e9180f74d92c83fc3cc1d2fc23f4855fb361c Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 19 Dec 2023 02:07:55 -0800 Subject: [PATCH 33/39] Prevent VPN server list persistence failures (#603) Task/Issue URL: https://app.asana.com/0/0/1206201299599597/f iOS PR: duckduckgo/iOS#2275 macOS PR: duckduckgo/macos-browser#1985 What kind of version bump will this require?: Major Description: This PR makes two changes: * The WireGuard invalid state error has been given a reason field, to allow insight into what state is invalid exactly * The server list store no longer prevents the file from being stored if the old one can't be removed; this appears to be a major source of unhandled errors --- .../Diagnostics/NetworkProtectionError.swift | 2 +- ...ror+NetworkProtectionErrorConvertible.swift | 4 ++-- .../PacketTunnelProvider.swift | 2 +- .../NetworkProtectionServerListStore.swift | 12 ++++++------ .../WireGuardKit/WireGuardAdapter.swift | 18 ++++++++++++------ 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift b/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift index 508afb3cd..911b94b63 100644 --- a/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift +++ b/Sources/NetworkProtection/Diagnostics/NetworkProtectionError.swift @@ -61,7 +61,7 @@ public enum NetworkProtectionError: LocalizedError { // Wireguard errors case wireGuardCannotLocateTunnelFileDescriptor - case wireGuardInvalidState + case wireGuardInvalidState(reason: String) case wireGuardDnsResolution case wireGuardSetNetworkSettings(Error) case startWireGuardBackend(Int32) diff --git a/Sources/NetworkProtection/Diagnostics/WireGuardAdapterError+NetworkProtectionErrorConvertible.swift b/Sources/NetworkProtection/Diagnostics/WireGuardAdapterError+NetworkProtectionErrorConvertible.swift index ae8b5486a..1f6e77559 100644 --- a/Sources/NetworkProtection/Diagnostics/WireGuardAdapterError+NetworkProtectionErrorConvertible.swift +++ b/Sources/NetworkProtection/Diagnostics/WireGuardAdapterError+NetworkProtectionErrorConvertible.swift @@ -23,8 +23,8 @@ extension WireGuardAdapterError: NetworkProtectionErrorConvertible { switch self { case .cannotLocateTunnelFileDescriptor: return .wireGuardCannotLocateTunnelFileDescriptor - case .invalidState: - return .wireGuardInvalidState + case .invalidState(let reason): + return .wireGuardInvalidState(reason: reason.rawValue) case .dnsResolution: return .wireGuardDnsResolution case .setNetworkSettings(let error): diff --git a/Sources/NetworkProtection/PacketTunnelProvider.swift b/Sources/NetworkProtection/PacketTunnelProvider.swift index ad85bb378..03ac8b0d5 100644 --- a/Sources/NetworkProtection/PacketTunnelProvider.swift +++ b/Sources/NetworkProtection/PacketTunnelProvider.swift @@ -891,7 +891,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider { resetRegistrationKey() let serverCache = NetworkProtectionServerListFileSystemStore(errorEvents: nil) - try? serverCache.removeServerList() + serverCache.removeServerList() try? tokenStore.deleteToken() diff --git a/Sources/NetworkProtection/Storage/NetworkProtectionServerListStore.swift b/Sources/NetworkProtection/Storage/NetworkProtectionServerListStore.swift index e3f36833f..9049e3392 100644 --- a/Sources/NetworkProtection/Storage/NetworkProtectionServerListStore.swift +++ b/Sources/NetworkProtection/Storage/NetworkProtectionServerListStore.swift @@ -163,26 +163,26 @@ public class NetworkProtectionServerListFileSystemStore: NetworkProtectionServer do { data = try Data(contentsOf: fileURL) } catch { - try removeServerList() + removeServerList() throw NetworkProtectionServerListStoreError.failedToReadServerList(error) } do { return try JSONDecoder().decode([NetworkProtectionServer].self, from: data) } catch { - try removeServerList() + removeServerList() throw NetworkProtectionServerListStoreError.failedToDecodeServerList(error) } } - public func removeServerList() throws { - if FileManager.default.fileExists(atPath: fileURL.relativePath) { - try FileManager.default.removeItem(at: fileURL) + public func removeServerList() { + if FileManager.default.fileExists(atPath: fileURL.path) { + try? FileManager.default.removeItem(at: fileURL) } } private func replaceServerList(with newList: [NetworkProtectionServer]) throws { - try removeServerList() + removeServerList() let serializedJSONData: Data diff --git a/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift b/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift index 6da060ae1..0b416abd3 100644 --- a/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift +++ b/Sources/NetworkProtection/WireGuardKit/WireGuardAdapter.swift @@ -7,12 +7,18 @@ import NetworkExtension import WireGuard import Common +public enum WireGuardAdapterErrorInvalidStateReason: String { + case alreadyStarted + case alreadyStopped + case updatedTunnelWhileStopped +} + public enum WireGuardAdapterError: Error { /// Failure to locate tunnel file descriptor. case cannotLocateTunnelFileDescriptor - /// Failure to perform an operation in such state. - case invalidState + /// Failure to perform an operation in such state. Includes a reason why the error was returned. + case invalidState(WireGuardAdapterErrorInvalidStateReason) /// Failure to resolve endpoints. case dnsResolution([DNSResolutionError]) @@ -263,7 +269,7 @@ public class WireGuardAdapter { public func start(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (WireGuardAdapterError?) -> Void) { workQueue.async { guard case .stopped = self.state else { - completionHandler(.invalidState) + completionHandler(.invalidState(.alreadyStarted)) return } @@ -307,7 +313,7 @@ public class WireGuardAdapter { break case .stopped: - completionHandler(.invalidState) + completionHandler(.invalidState(.alreadyStopped)) return } @@ -323,14 +329,14 @@ public class WireGuardAdapter { /// Update runtime configuration. /// - Parameters: /// - tunnelConfiguration: tunnel configuration. - /// - reassert: wether the connection should reassert or not. + /// - reassert: whether the connection should reassert or not. /// - completionHandler: completion handler. public func update(tunnelConfiguration: TunnelConfiguration, reassert: Bool = true, completionHandler: @escaping (WireGuardAdapterError?) -> Void) { workQueue.async { if case .stopped = self.state { - completionHandler(.invalidState) + completionHandler(.invalidState(.updatedTunnelWhileStopped)) return } From fee9085e56478736f57ccaf270a133ac40789228 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 19 Dec 2023 12:10:57 +0100 Subject: [PATCH 34/39] Add new logger (#604) --- Sources/Common/Logging.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Common/Logging.swift b/Sources/Common/Logging.swift index fe93cb221..0eb1ed313 100644 --- a/Sources/Common/Logging.swift +++ b/Sources/Common/Logging.swift @@ -39,6 +39,7 @@ extension OSLog { case userScripts = "User Scripts" case passwordManager = "Password Manager" case remoteMessaging = "Remote Messaging" + case subscription = "Subscription" } #if DEBUG @@ -50,6 +51,7 @@ extension OSLog { @OSLogWrapper(.userScripts) public static var userScripts @OSLogWrapper(.passwordManager) public static var passwordManager @OSLogWrapper(.remoteMessaging) public static var remoteMessaging + @OSLogWrapper(.subscription) public static var subscription public static var enabledLoggingCategories = Set() From e5c9e31b19b3cf78e2b704a60882ba24b9bc680d Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Wed, 20 Dec 2023 11:44:43 +0100 Subject: [PATCH 35/39] Add Sync Success Rate pixel (#605) Task/Issue URL: https://app.asana.com/0/0/1206206145252506/f https://app.asana.com/0/0/1204831721662171/f iOS PR: duckduckgo/iOS#2277 macOS PR: duckduckgo/macos-browser#1993 What kind of version bump will this require?: Major Optional: Tech Design URL: CC: Description: Add Sync succes rate pixel. --- Package.swift | 10 +- Sources/Bookmarks/BookmarkErrors.swift | 1 - Sources/Bookmarks/BookmarkListViewModel.swift | 6 +- Sources/DDGSync/DDGSync.swift | 12 ++ Sources/DDGSync/DDGSyncing.swift | 5 + Sources/DDGSync/SyncDailyStats.swift | 130 +++++++++++++++ .../internal/ProductionDependencies.swift | 3 +- .../DDGSync/internal/SyncDependencies.swift | 7 +- Sources/DDGSync/internal/SyncOperation.swift | 4 +- Sources/Persistence/KeyValueStoring.swift | 2 + .../MockKeyValueStore.swift} | 23 ++- .../BookmarkListViewModelTests.swift | 5 - .../AdClickAttributionCounterTests.swift | 19 +-- .../DDGSyncTests/DDGSyncLifecycleTests.swift | 15 +- Tests/DDGSyncTests/DDGSyncTests.swift | 1 + Tests/DDGSyncTests/Mocks/Mocks.swift | 18 +-- Tests/DDGSyncTests/SyncDailyStatsTests.swift | 153 ++++++++++++++++++ 17 files changed, 342 insertions(+), 72 deletions(-) create mode 100644 Sources/DDGSync/SyncDailyStats.swift rename Sources/{DDGSync/internal/KeyValueStore.swift => TestUtils/MockKeyValueStore.swift} (54%) create mode 100644 Tests/DDGSyncTests/SyncDailyStatsTests.swift diff --git a/Package.swift b/Package.swift index f150b03be..a9b092e40 100644 --- a/Package.swift +++ b/Package.swift @@ -14,6 +14,7 @@ let package = Package( // Exported libraries .library(name: "BrowserServicesKit", targets: ["BrowserServicesKit"]), .library(name: "Common", targets: ["Common"]), + .library(name: "TestUtils", targets: ["TestUtils"]), .library(name: "DDGSync", targets: ["DDGSync"]), .library(name: "Persistence", targets: ["Persistence"]), .library(name: "Bookmarks", targets: ["Bookmarks"]), @@ -256,7 +257,8 @@ let package = Package( .target( name: "TestUtils", dependencies: [ - "Networking" + "Networking", + "Persistence" ], plugins: [.plugin(name: "SwiftLintPlugin")] ), @@ -328,7 +330,8 @@ let package = Package( dependencies: [ "BrowserServicesKit", "RemoteMessaging", // Move tests later (lots of test dependencies in BSK) - "SecureStorageTestsUtils" + "SecureStorageTestsUtils", + "TestUtils" ], resources: [ .copy("Resources") @@ -338,7 +341,8 @@ let package = Package( .testTarget( name: "DDGSyncTests", dependencies: [ - "DDGSync" + "DDGSync", + "TestUtils" ], plugins: [.plugin(name: "SwiftLintPlugin")] ), diff --git a/Sources/Bookmarks/BookmarkErrors.swift b/Sources/Bookmarks/BookmarkErrors.swift index 0d8c81f11..78020fb57 100644 --- a/Sources/Bookmarks/BookmarkErrors.swift +++ b/Sources/Bookmarks/BookmarkErrors.swift @@ -46,7 +46,6 @@ public enum BookmarksModelError: Error, Equatable { case bookmarksListMissingFolder case bookmarksListIndexNotMatchingBookmark case favoritesListIndexNotMatchingBookmark - case orphanedBookmarksPresent case editorNewParentMissing } diff --git a/Sources/Bookmarks/BookmarkListViewModel.swift b/Sources/Bookmarks/BookmarkListViewModel.swift index 8e1edb50a..d19d34efe 100644 --- a/Sources/Bookmarks/BookmarkListViewModel.swift +++ b/Sources/Bookmarks/BookmarkListViewModel.swift @@ -287,11 +287,7 @@ public class BookmarkListViewModel: BookmarkListInteracting, ObservableObject { }() if shouldFetchRootFolder { - let orphanedBookmarks = BookmarkUtils.fetchOrphanedEntities(context) - if !orphanedBookmarks.isEmpty { - errorEvents?.fire(.orphanedBookmarksPresent) - } - folderBookmarks += orphanedBookmarks + folderBookmarks += BookmarkUtils.fetchOrphanedEntities(context) } return folderBookmarks } diff --git a/Sources/DDGSync/DDGSync.swift b/Sources/DDGSync/DDGSync.swift index adde7a4a8..ffae67f50 100644 --- a/Sources/DDGSync/DDGSync.swift +++ b/Sources/DDGSync/DDGSync.swift @@ -42,6 +42,8 @@ public class DDGSync: DDGSyncing { dependencies.scheduler } + public let syncDailyStats = SyncDailyStats() + @Published public var isSyncInProgress: Bool = false public var isSyncInProgressPublisher: AnyPublisher { @@ -258,6 +260,15 @@ public class DDGSync: DDGSyncing { self?.isSyncInProgress = isInProgress }) + syncDidFinishCancellable = syncQueue.syncDidFinishPublisher + .sink(receiveValue: { [weak self] result in + var syncError: SyncOperationError? + if case let .failure(error) = result { + syncError = error as? SyncOperationError + } + self?.syncDailyStats.onSyncFinished(with: syncError) + }) + startSyncCancellable = dependencies.scheduler.startSyncPublisher .sink { [weak self] in self?.syncQueue?.startSync() @@ -309,5 +320,6 @@ public class DDGSync: DDGSyncing { private var syncQueue: SyncQueue? private var syncQueueCancellable: AnyCancellable? + private var syncDidFinishCancellable: AnyCancellable? private var syncQueueRequestErrorCancellable: AnyCancellable? } diff --git a/Sources/DDGSync/DDGSyncing.swift b/Sources/DDGSync/DDGSyncing.swift index 76869d90c..4dcae0921 100644 --- a/Sources/DDGSync/DDGSyncing.swift +++ b/Sources/DDGSync/DDGSyncing.swift @@ -74,6 +74,11 @@ public protocol DDGSyncing: DDGSyncingDebuggingSupport { */ var scheduler: Scheduling { get } + /** + Used to aggregate success and error stats of sync operations. + */ + var syncDailyStats: SyncDailyStats { get } + /** Returns true if there is an ongoing sync operation. */ diff --git a/Sources/DDGSync/SyncDailyStats.swift b/Sources/DDGSync/SyncDailyStats.swift new file mode 100644 index 000000000..de7436850 --- /dev/null +++ b/Sources/DDGSync/SyncDailyStats.swift @@ -0,0 +1,130 @@ +// +// SyncDailyStats.swift +// +// Copyright © 2022 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 Persistence + +public class SyncDailyStats { + + public enum Constants { + public static let dailyStatsDictKey = "dailyStatsDictKey" + public static let lastSentDate = "dailyStats_last_sent_date" + + // Dict Parameters + public static let syncCountParam = "sync_count" + public static let syncDateParam = "date" + } + + private let store: KeyValueStoring + private let lock = NSLock() + + init(store: KeyValueStoring = UserDefaults()) { + self.store = store + } + + func onSyncFinished(with error: SyncOperationError?) { + + var updatedKeys = [Constants.syncCountParam] + + if let perFeatureErrors = error?.perFeatureErrors { + for (feature, error) in perFeatureErrors { + if let featureError = error as? SyncError, + let knownError = ErrorType(syncError: featureError) { + updatedKeys.append(knownError.key(for: feature)) + } + } + } + + lock.lock() + defer { lock.unlock() } + + var storeValues: [String: Int] = (store.object(forKey: Constants.dailyStatsDictKey) as? [String: Int]) ?? [:] + + for updatedKey in updatedKeys { + storeValues[updatedKey] = (storeValues[updatedKey] ?? 0) + 1 + } + + store.set(storeValues, forKey: Constants.dailyStatsDictKey) + } + + public func sendStatsIfNeeded(currentDate: Date = Date(), + handler: ([String: String]) -> Void) { + guard let lastDate = store.object(forKey: Constants.lastSentDate) as? Date else { + store.set(currentDate, forKey: Constants.lastSentDate) + return + } + + guard !Calendar.current.isDateInToday(lastDate) else { return } + + lock.lock() + defer { lock.unlock() } + + if let currentStats = (store.object(forKey: Constants.dailyStatsDictKey) as? [String: Int]) { + var parameters = currentStats.mapValues({ "\($0)" }) + + let dateFormater = DateFormatter() + dateFormater.dateFormat = "yyyy-MM-dd" + parameters[Constants.syncDateParam] = dateFormater.string(from: lastDate) + handler(parameters) + } + + store.removeObject(forKey: Constants.dailyStatsDictKey) + store.set(currentDate, forKey: Constants.lastSentDate) + } + + enum ErrorType { + case objectLimitExceeded + case requestSizeLimitExceeded + case validation + case requestLimitExceeded + + init?(syncError: SyncError) { + guard case .unexpectedStatusCode(let code) = syncError else { return nil } + + switch code { + case 409: + self = .objectLimitExceeded + case 413: + self = .requestSizeLimitExceeded + case 400: + self = .validation + case 418, 429: + self = .requestLimitExceeded + default: + return nil + } + } + + var asString: String { + switch self { + case .objectLimitExceeded: + return "object_limit_exceeded" + case .requestSizeLimitExceeded: + return "request_size_limit_exceeded" + case .validation: + return "validation_error" + case .requestLimitExceeded: + return "too_many_requests" + } + } + + func key(for feature: Feature) -> String { + "\(feature.name)_\(asString)_count" + } + } +} diff --git a/Sources/DDGSync/internal/ProductionDependencies.swift b/Sources/DDGSync/internal/ProductionDependencies.swift index c40bfa019..aff1764de 100644 --- a/Sources/DDGSync/internal/ProductionDependencies.swift +++ b/Sources/DDGSync/internal/ProductionDependencies.swift @@ -18,6 +18,7 @@ import Foundation import Common +import Persistence struct ProductionDependencies: SyncDependencies { @@ -40,7 +41,7 @@ struct ProductionDependencies: SyncDependencies { self.init(fileStorageUrl: FileManager.default.applicationSupportDirectoryForComponent(named: "Sync"), serverEnvironment: serverEnvironment, - keyValueStore: KeyValueStore(), + keyValueStore: UserDefaults(), secureStore: SecureStorage(), errorEvents: errorEvents, log: log()) diff --git a/Sources/DDGSync/internal/SyncDependencies.swift b/Sources/DDGSync/internal/SyncDependencies.swift index 937614cb2..d88f643f8 100644 --- a/Sources/DDGSync/internal/SyncDependencies.swift +++ b/Sources/DDGSync/internal/SyncDependencies.swift @@ -19,6 +19,7 @@ import Foundation import Combine import Common +import Persistence protocol SyncDependenciesDebuggingSupport { func updateServerEnvironment(_ serverEnvironment: ServerEnvironment) @@ -54,12 +55,6 @@ protocol AccountManaging { } -protocol KeyValueStoring { - - func object(forKey: String) -> Any? - func set(_ value: Any?, forKey: String) -} - protocol SecureStoring { func persistAccount(_ account: SyncAccount) throws func account() throws -> SyncAccount? diff --git a/Sources/DDGSync/internal/SyncOperation.swift b/Sources/DDGSync/internal/SyncOperation.swift index 498a48624..b4e84e756 100644 --- a/Sources/DDGSync/internal/SyncOperation.swift +++ b/Sources/DDGSync/internal/SyncOperation.swift @@ -166,9 +166,7 @@ final class SyncOperation: Operation { fetchOnly: fetchOnly, timestamp: clientTimestamp) default: - let error = SyncError.unexpectedStatusCode(httpResult.response.statusCode) - didReceiveHTTPRequestError?(error) - throw FeatureError(feature: dataProvider.feature, underlyingError: error) + throw SyncError.unexpectedStatusCode(httpResult.response.statusCode) } } catch is CancellationError { os_log(.debug, log: self.log, "Syncing %{public}s cancelled", dataProvider.feature.name) diff --git a/Sources/Persistence/KeyValueStoring.swift b/Sources/Persistence/KeyValueStoring.swift index f1e912423..1b2c58a57 100644 --- a/Sources/Persistence/KeyValueStoring.swift +++ b/Sources/Persistence/KeyValueStoring.swift @@ -26,3 +26,5 @@ public protocol KeyValueStoring { func removeObject(forKey defaultName: String) } + +extension UserDefaults: KeyValueStoring { } diff --git a/Sources/DDGSync/internal/KeyValueStore.swift b/Sources/TestUtils/MockKeyValueStore.swift similarity index 54% rename from Sources/DDGSync/internal/KeyValueStore.swift rename to Sources/TestUtils/MockKeyValueStore.swift index 5100945b6..47b81a5c3 100644 --- a/Sources/DDGSync/internal/KeyValueStore.swift +++ b/Sources/TestUtils/MockKeyValueStore.swift @@ -1,7 +1,7 @@ // -// KeyValueStore.swift +// MockKeyValueStore.swift // -// Copyright © 2022 DuckDuckGo. All rights reserved. +// 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. @@ -17,15 +17,24 @@ // import Foundation +import Persistence -struct KeyValueStore: KeyValueStoring { +public class MockKeyValueStore: KeyValueStoring { - func object(forKey key: String) -> Any? { - return UserDefaults().object(forKey: key) + var store = [String: Any?]() + + public init() { } + + public func object(forKey defaultName: String) -> Any? { + return store[defaultName] as Any? + } + + public func set(_ value: Any?, forKey defaultName: String) { + store[defaultName] = value } - func set(_ value: Any?, forKey key: String) { - UserDefaults().set(value, forKey: key) + public func removeObject(forKey defaultName: String) { + store[defaultName] = nil } } diff --git a/Tests/BookmarksTests/BookmarkListViewModelTests.swift b/Tests/BookmarksTests/BookmarkListViewModelTests.swift index 3966b53b2..7cb61c8d0 100644 --- a/Tests/BookmarksTests/BookmarkListViewModelTests.swift +++ b/Tests/BookmarksTests/BookmarkListViewModelTests.swift @@ -165,7 +165,6 @@ final class BookmarkListViewModelTests: XCTestCase { Bookmark(id: "2") Bookmark(id: "1") }) - XCTAssertEqual(firedEvents, [.orphanedBookmarksPresent]) } } @@ -201,7 +200,6 @@ final class BookmarkListViewModelTests: XCTestCase { Bookmark(id: "4") Bookmark(id: "6", isOrphaned: true) }) - XCTAssertEqual(firedEvents, [.orphanedBookmarksPresent]) } } @@ -237,7 +235,6 @@ final class BookmarkListViewModelTests: XCTestCase { Bookmark(id: "3") Bookmark(id: "6", isOrphaned: true) }) - XCTAssertEqual(firedEvents, [.orphanedBookmarksPresent]) } } @@ -273,7 +270,6 @@ final class BookmarkListViewModelTests: XCTestCase { Bookmark(id: "5", isOrphaned: true) Bookmark(id: "6", isOrphaned: true) }) - XCTAssertEqual(firedEvents, [.orphanedBookmarksPresent]) } } @@ -309,7 +305,6 @@ final class BookmarkListViewModelTests: XCTestCase { Bookmark(id: "5", isOrphaned: true) Bookmark(id: "6", isOrphaned: true) }) - XCTAssertEqual(firedEvents, [.orphanedBookmarksPresent]) } } diff --git a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionCounterTests.swift b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionCounterTests.swift index 017050911..592d35498 100644 --- a/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionCounterTests.swift +++ b/Tests/BrowserServicesKitTests/ContentBlocker/AdClickAttribution/AdClickAttributionCounterTests.swift @@ -18,26 +18,9 @@ import XCTest import Persistence +import TestUtils @testable import BrowserServicesKit -class MockKeyValueStore: KeyValueStoring { - - var store = [String: Any?]() - - func object(forKey defaultName: String) -> Any? { - return store[defaultName] as Any? - } - - func set(_ value: Any?, forKey defaultName: String) { - store[defaultName] = value - } - - func removeObject(forKey defaultName: String) { - store[defaultName] = nil - } - -} - class AdClickAttributionCounterTests: XCTestCase { func testWhenEventIsDetectedCounterIsIncremented() { diff --git a/Tests/DDGSyncTests/DDGSyncLifecycleTests.swift b/Tests/DDGSyncTests/DDGSyncLifecycleTests.swift index 55066e886..94dadb9af 100644 --- a/Tests/DDGSyncTests/DDGSyncLifecycleTests.swift +++ b/Tests/DDGSyncTests/DDGSyncLifecycleTests.swift @@ -19,6 +19,7 @@ import Combine import Common import XCTest +import TestUtils @testable import DDGSync final class DDGSyncLifecycleTests: XCTestCase { @@ -26,10 +27,6 @@ final class DDGSyncLifecycleTests: XCTestCase { var dataProvidersSource: MockDataProvidersSource! var dependencies: MockSyncDependencies! - var mockKeyValueStore: MockKeyValueStore { - dependencies.keyValueStore as! MockKeyValueStore - } - var secureStorageStub: SecureStorageStub { dependencies.secureStore as! SecureStorageStub } @@ -47,7 +44,7 @@ final class DDGSyncLifecycleTests: XCTestCase { func testWhenInitializingAndOffThenStateIsInactive() { secureStorageStub.theAccount = nil - mockKeyValueStore.isSyncEnabled = false + dependencies.keyValueStore.set(false, forKey: DDGSync.Constants.syncEnabledKey) let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) XCTAssertEqual(syncService.authState, .initializing) @@ -58,6 +55,7 @@ final class DDGSyncLifecycleTests: XCTestCase { func testWhenInitializingAndOnThenStateIsActive() { secureStorageStub.theAccount = .mock + dependencies.keyValueStore.set(true, forKey: DDGSync.Constants.syncEnabledKey) let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) XCTAssertEqual(syncService.authState, .initializing) @@ -68,7 +66,6 @@ final class DDGSyncLifecycleTests: XCTestCase { func testWhenInitializingAndAfterReinstallThenStateIsInactive() { secureStorageStub.theAccount = .mock - mockKeyValueStore.isSyncEnabled = nil let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) XCTAssertEqual(syncService.authState, .initializing) @@ -80,7 +77,7 @@ final class DDGSyncLifecycleTests: XCTestCase { func testWhenInitializingAndKeysBeenRemovedThenStateIsInactive() { secureStorageStub.theAccount = nil - mockKeyValueStore.isSyncEnabled = true + dependencies.keyValueStore.set(true, forKey: DDGSync.Constants.syncEnabledKey) let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) XCTAssertEqual(syncService.authState, .initializing) @@ -99,6 +96,8 @@ final class DDGSyncLifecycleTests: XCTestCase { secureStorageStub.theAccount = .mock secureStorageStub.mockReadError = expectedError + dependencies.keyValueStore.set(true, forKey: DDGSync.Constants.syncEnabledKey) + let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) XCTAssertEqual(syncService.authState, .initializing) syncService.initializeIfNeeded() @@ -111,6 +110,8 @@ final class DDGSyncLifecycleTests: XCTestCase { secureStorageStub.theAccount = .mock secureStorageStub.mockWriteError = expectedError + dependencies.keyValueStore.set(true, forKey: DDGSync.Constants.syncEnabledKey) + let syncService = DDGSync(dataProvidersSource: dataProvidersSource, dependencies: dependencies) XCTAssertEqual(syncService.authState, .initializing) syncService.initializeIfNeeded() diff --git a/Tests/DDGSyncTests/DDGSyncTests.swift b/Tests/DDGSyncTests/DDGSyncTests.swift index 3044d7f79..b8c81a3f8 100644 --- a/Tests/DDGSyncTests/DDGSyncTests.swift +++ b/Tests/DDGSyncTests/DDGSyncTests.swift @@ -55,6 +55,7 @@ final class DDGSyncTests: XCTestCase { ] (dependencies.secureStore as! SecureStorageStub).theAccount = .mock + dependencies.keyValueStore.set(true, forKey: DDGSync.Constants.syncEnabledKey) } override func tearDownWithError() throws { diff --git a/Tests/DDGSyncTests/Mocks/Mocks.swift b/Tests/DDGSyncTests/Mocks/Mocks.swift index d0f8d290e..08ea0168a 100644 --- a/Tests/DDGSyncTests/Mocks/Mocks.swift +++ b/Tests/DDGSyncTests/Mocks/Mocks.swift @@ -19,6 +19,8 @@ import Combine import Common import Foundation +import Persistence +import TestUtils @testable import DDGSync extension SyncAccount { @@ -130,22 +132,6 @@ class MockErrorHandler: EventMapping { } } -class MockKeyValueStore: KeyValueStoring { - var isSyncEnabled: Bool? = true - - func object(forKey: String) -> Any? { - if forKey == DDGSync.Constants.syncEnabledKey { - return isSyncEnabled - } - return nil - } - func set(_ value: Any?, forKey: String) { - if forKey == DDGSync.Constants.syncEnabledKey, let boolValue = value as? Bool { - isSyncEnabled = boolValue - } - } -} - struct MockSyncDependencies: SyncDependencies, SyncDependenciesDebuggingSupport { var endpoints: Endpoints = Endpoints(baseURL: URL(string: "https://dev.null")!) var account: AccountManaging = AccountManagingMock() diff --git a/Tests/DDGSyncTests/SyncDailyStatsTests.swift b/Tests/DDGSyncTests/SyncDailyStatsTests.swift new file mode 100644 index 000000000..710e70001 --- /dev/null +++ b/Tests/DDGSyncTests/SyncDailyStatsTests.swift @@ -0,0 +1,153 @@ +// +// SyncDailyStatsTests.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import TestUtils +@testable import DDGSync + +class SyncDailyStatsTests: XCTestCase { + + static let featureA = Feature(name: "featureA") + static let featureB = Feature(name: "featureB") + + static let error1 = SyncError.unexpectedStatusCode(400) + static let error2 = SyncError.unexpectedStatusCode(413) + + let store = MockKeyValueStore() + var stats: SyncDailyStats! + + var statusDict: [String: Int]? { + store.object(forKey: SyncDailyStats.Constants.dailyStatsDictKey) as? [String: Int] + } + + override func setUp() { + super.setUp() + + stats = SyncDailyStats(store: store) + + XCTAssertNil(store.object(forKey: SyncDailyStats.Constants.lastSentDate)) + XCTAssertNil(store.object(forKey: SyncDailyStats.Constants.syncCountParam)) + } + + func testWhenSyncIsFinishedThenCountIncreases() { + stats.onSyncFinished(with: nil) + + XCTAssertEqual(statusDict?[SyncDailyStats.Constants.syncCountParam], 1) + + stats.onSyncFinished(with: nil) + + XCTAssertEqual(statusDict?[SyncDailyStats.Constants.syncCountParam], 2) + } + + func testWhenErrorIsFoundThenCountsIncrease() { + stats.onSyncFinished(with: SyncOperationError(featureErrors: [.init(feature: Self.featureA, + underlyingError: Self.error1)])) + + XCTAssertEqual(statusDict?[SyncDailyStats.Constants.syncCountParam], 1) + XCTAssertEqual(statusDict?[SyncDailyStats.ErrorType(syncError: Self.error1)?.key(for: Self.featureA) ?? ""], 1) + + stats.onSyncFinished(with: SyncOperationError(featureErrors: [.init(feature: Self.featureA, + underlyingError: Self.error1), + .init(feature: Self.featureB, + underlyingError: Self.error2)])) + + XCTAssertEqual(statusDict?[SyncDailyStats.Constants.syncCountParam], 2) + XCTAssertEqual(statusDict?[SyncDailyStats.ErrorType(syncError: Self.error1)?.key(for: Self.featureA) ?? ""], 2) + XCTAssertEqual(statusDict?[SyncDailyStats.ErrorType(syncError: Self.error2)?.key(for: Self.featureB) ?? ""], 1) + } + + func testWhenNewInstallThenSendNothing() { + + stats.sendStatsIfNeeded { _ in + XCTFail("Should not execute") + } + + XCTAssertNotNil(store.object(forKey: SyncDailyStats.Constants.lastSentDate)) + } + + func testWhenSameDayThenSendNothing() { + stats.sendStatsIfNeeded { _ in + XCTFail("Should not execute") + } + + let firstStoredDate = store.object(forKey: SyncDailyStats.Constants.lastSentDate) as? Date + XCTAssertNotNil(firstStoredDate) + + Thread.sleep(forTimeInterval: 0.1) + + stats.sendStatsIfNeeded { _ in + XCTFail("Should not execute") + } + + XCTAssertNotNil(store.object(forKey: SyncDailyStats.Constants.lastSentDate)) + XCTAssertEqual(store.object(forKey: SyncDailyStats.Constants.lastSentDate) as? Date, firstStoredDate) + } + + func testWhenNextDayThenSendData() { + let currentDate = Date() + guard let yesterday = Calendar.current.date(byAdding: DateComponents(day: -1), to: currentDate) else { + XCTFail("Could not create date") + return + } + + XCTAssertFalse(Calendar.current.isDate(currentDate, inSameDayAs: yesterday)) + XCTAssertFalse(Calendar.current.isDateInToday(yesterday)) + + stats.sendStatsIfNeeded(currentDate: yesterday) { _ in + XCTFail("Should not execute") + } + + XCTAssertEqual(store.object(forKey: SyncDailyStats.Constants.lastSentDate) as? Date, yesterday) + + let exp = expectation(description: "Should send data") + + stats.onSyncFinished(with: nil) + stats.sendStatsIfNeeded(currentDate: currentDate) { data in + XCTAssertEqual(data[SyncDailyStats.Constants.syncCountParam], "1") + exp.fulfill() + } + + XCTAssertEqual(store.object(forKey: SyncDailyStats.Constants.lastSentDate) as? Date, currentDate) + + wait(for: [exp], timeout: 1) + } + + func testWhenNextDayButNoDataThenDontSendData() { + let currentDate = Date() + guard let yesterday = Calendar.current.date(byAdding: DateComponents(day: -1), to: currentDate) else { + XCTFail("Could not create date") + return + } + + XCTAssertFalse(Calendar.current.isDate(currentDate, inSameDayAs: yesterday)) + XCTAssertFalse(Calendar.current.isDateInToday(yesterday)) + + stats.sendStatsIfNeeded(currentDate: yesterday) { _ in + XCTFail("Should not execute") + } + + XCTAssertEqual(store.object(forKey: SyncDailyStats.Constants.lastSentDate) as? Date, yesterday) + + stats.sendStatsIfNeeded(currentDate: currentDate) { _ in + XCTFail("Should not execute - no data") + } + + XCTAssertEqual(store.object(forKey: SyncDailyStats.Constants.lastSentDate) as? Date, currentDate) + } + +} From 308abf4ebf170dc73d9f1a8a1730ed3170bed2d5 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 20 Dec 2023 16:02:03 +0100 Subject: [PATCH 36/39] Fix Networking import into TestUtils (#609) Task/Issue URL: https://app.asana.com/0/0/1206206145252506/f --- Sources/TestUtils/Utils/HTTPURLResponseExtension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TestUtils/Utils/HTTPURLResponseExtension.swift b/Sources/TestUtils/Utils/HTTPURLResponseExtension.swift index 9135b285e..211e32c0c 100644 --- a/Sources/TestUtils/Utils/HTTPURLResponseExtension.swift +++ b/Sources/TestUtils/Utils/HTTPURLResponseExtension.swift @@ -17,7 +17,7 @@ // import Foundation -@testable import Networking +import Networking extension HTTPURLResponse { From d671accf1bf7097c4e7f5cd55cd1c6dfa005cf92 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 20 Dec 2023 16:36:41 +0100 Subject: [PATCH 37/39] Add Sync feature flags (#607) Task/Issue URL: https://app.asana.com/0/0/1206046777189407/f Description: This change add support for Sync feature in Privacy Configuration, together with 4 subfeatures defining availability of various parts of Sync experience. DDGSyncing gets a read-only feature flag variable as well as a publisher. PrivacyConfigurationManager is now a Sync dependency, and DDGSync takes care internally of listening to Privacy Config changes and updating feature flags as needed. Feature flag responsible for actual data syncing is handled internally in DDGSync by cancelling all pending sync operations and disabling adding new operations. Other feature flags should be handled by client apps. --- Package.resolved | 4 +- Package.swift | 1 + .../Features/PrivacyFeature.swift | 12 +++ Sources/DDGSync/DDGSync.swift | 34 +++++++- Sources/DDGSync/DDGSyncing.swift | 15 +++- Sources/DDGSync/SyncFeatureFlags.swift | 87 +++++++++++++++++++ .../internal/ProductionDependencies.swift | 15 +++- .../DDGSync/internal/SyncDependencies.swift | 4 +- Sources/DDGSync/internal/SyncQueue.swift | 15 ++++ Tests/DDGSyncTests/Mocks/Mocks.swift | 43 +++++++++ Tests/DDGSyncTests/SyncQueueTests.swift | 25 ++++++ 11 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 Sources/DDGSync/SyncFeatureFlags.swift diff --git a/Package.resolved b/Package.resolved index c65a105fd..2476554ae 100644 --- a/Package.resolved +++ b/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version" : "1.2.3" + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" } }, { diff --git a/Package.swift b/Package.swift index a9b092e40..23af46854 100644 --- a/Package.swift +++ b/Package.swift @@ -127,6 +127,7 @@ let package = Package( .target( name: "DDGSync", dependencies: [ + "BrowserServicesKit", "Common", .product(name: "DDGSyncCrypto", package: "sync_crypto"), "Networking" diff --git a/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift b/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift index d8e85a604..d15e4eca9 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift @@ -41,6 +41,7 @@ public enum PrivacyFeature: String { case newTabContinueSetUp case networkProtection case dbp + case sync } /// An abstraction to be implemented by any "subfeature" of a given `PrivacyConfiguration` feature. @@ -82,3 +83,14 @@ public enum DBPSubfeature: String, Equatable, PrivacySubfeature { case waitlist case waitlistBetaActive } + +public enum SyncSubfeature: String, PrivacySubfeature { + public var parent: PrivacyFeature { + .sync + } + + case level0ShowSync + case level1AllowDataSyncing + case level2AllowSetupFlows + case level3AllowCreateAccount +} diff --git a/Sources/DDGSync/DDGSync.swift b/Sources/DDGSync/DDGSync.swift index ffae67f50..0df8e1e3f 100644 --- a/Sources/DDGSync/DDGSync.swift +++ b/Sources/DDGSync/DDGSync.swift @@ -16,15 +16,21 @@ // limitations under the License. // -import Foundation +import BrowserServicesKit import Combine -import DDGSyncCrypto import Common +import DDGSyncCrypto +import Foundation public class DDGSync: DDGSyncing { public static let bundle = Bundle.module + @Published public private(set) var featureFlags: SyncFeatureFlags = .all + public var featureFlagsPublisher: AnyPublisher { + $featureFlags.eraseToAnyPublisher() + } + enum Constants { public static let syncEnabledKey = "com.duckduckgo.sync.enabled" } @@ -55,9 +61,15 @@ public class DDGSync: DDGSyncing { /// This is the constructor intended for use by app clients. public convenience init(dataProvidersSource: DataProvidersSource, errorEvents: EventMapping, + privacyConfigurationManager: PrivacyConfigurationManaging, log: @escaping @autoclosure () -> OSLog = .disabled, environment: ServerEnvironment = .production) { - let dependencies = ProductionDependencies(serverEnvironment: environment, errorEvents: errorEvents, log: log()) + let dependencies = ProductionDependencies( + serverEnvironment: environment, + privacyConfigurationManager: privacyConfigurationManager, + errorEvents: errorEvents, + log: log() + ) self.init(dataProvidersSource: dataProvidersSource, dependencies: dependencies) } @@ -189,6 +201,16 @@ public class DDGSync: DDGSyncing { init(dataProvidersSource: DataProvidersSource, dependencies: SyncDependencies) { self.dataProvidersSource = dataProvidersSource self.dependencies = dependencies + + featureFlagsCancellable = self.dependencies.privacyConfigurationManager.updatesPublisher + .compactMap { [weak self] in + self?.dependencies.privacyConfigurationManager.privacyConfig + } + .prepend(dependencies.privacyConfigurationManager.privacyConfig) + .map(SyncFeatureFlags.init) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: \.featureFlags, onWeaklyHeld: self) } public func initializeIfNeeded() { @@ -229,6 +251,7 @@ public class DDGSync: DDGSyncing { dependencies.scheduler.isEnabled = false startSyncCancellable?.cancel() syncQueueCancellable?.cancel() + isDataSyncingFeatureFlagEnabledCancellable?.cancel() try syncQueue?.dataProviders.forEach { try $0.deregisterFeature() } syncQueue = nil authState = .inactive @@ -291,6 +314,9 @@ public class DDGSync: DDGSyncing { self?.syncQueue?.resumeQueue() } + isDataSyncingFeatureFlagEnabledCancellable = featureFlagsPublisher.prepend(featureFlags).map { $0.contains(.dataSyncing) } + .assign(to: \.isDataSyncingFeatureFlagEnabled, onWeaklyHeld: syncQueue) + dependencies.scheduler.isEnabled = true self.syncQueue = syncQueue } @@ -317,6 +343,8 @@ public class DDGSync: DDGSyncing { private var startSyncCancellable: AnyCancellable? private var cancelSyncCancellable: AnyCancellable? private var resumeSyncCancellable: AnyCancellable? + private var featureFlagsCancellable: AnyCancellable? + private var isDataSyncingFeatureFlagEnabledCancellable: AnyCancellable? private var syncQueue: SyncQueue? private var syncQueueCancellable: AnyCancellable? diff --git a/Sources/DDGSync/DDGSyncing.swift b/Sources/DDGSync/DDGSyncing.swift index 4dcae0921..71d8dfcb3 100644 --- a/Sources/DDGSync/DDGSyncing.swift +++ b/Sources/DDGSync/DDGSyncing.swift @@ -16,9 +16,10 @@ // limitations under the License. // -import Foundation -import DDGSyncCrypto +import BrowserServicesKit import Combine +import DDGSyncCrypto +import Foundation public enum SyncAuthState: String, Sendable, Codable { /// Sync engine is not initialized. @@ -48,6 +49,16 @@ public protocol DDGSyncing: DDGSyncingDebuggingSupport { var dataProvidersSource: DataProvidersSource? { get set } + /** + Describes current availability of sync features. + */ + var featureFlags: SyncFeatureFlags { get } + + /** + Emits changes to current availability of sync features + */ + var featureFlagsPublisher: AnyPublisher { get } + /** Describes current state of sync account. diff --git a/Sources/DDGSync/SyncFeatureFlags.swift b/Sources/DDGSync/SyncFeatureFlags.swift new file mode 100644 index 000000000..88eea2783 --- /dev/null +++ b/Sources/DDGSync/SyncFeatureFlags.swift @@ -0,0 +1,87 @@ +// +// SyncFeatureFlags.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 BrowserServicesKit +import Foundation + +/** + * This enum describes available Sync features. + */ +public struct SyncFeatureFlags: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + // MARK: - Individual Flags + + /// Sync UI is visible + public static let userInterface = SyncFeatureFlags(rawValue: 1 << 0) + + /// Data syncing is available + public static let dataSyncing = SyncFeatureFlags(rawValue: 1 << 1) + + /// Logging in to existing accounts is available (connect flows + account recovery) + public static let accountLogin = SyncFeatureFlags(rawValue: 1 << 2) + + /// Creating new accounts is available + public static let accountCreation = SyncFeatureFlags(rawValue: 1 << 4) + + // MARK: - Helper Flags + + public static let connectFlows = SyncFeatureFlags.accountLogin + public static let accountRecovery = SyncFeatureFlags.accountLogin + + // MARK: - Support levels + + /// Used when all feature flags are disabled + public static let unavailable: SyncFeatureFlags = [] + + /// Level 0 feature flag as defined in Privacy Configuration + public static let level0ShowSync: SyncFeatureFlags = [.userInterface] + /// Level 1 feature flag as defined in Privacy Configuration + public static let level1AllowDataSyncing: SyncFeatureFlags = [.userInterface, .dataSyncing] + /// Level 2 feature flag as defined in Privacy Configuration + public static let level2AllowSetupFlows: SyncFeatureFlags = [.userInterface, .dataSyncing, .accountLogin] + /// Level 3 feature flag as defined in Privacy Configuration + public static let level3AllowCreateAccount: SyncFeatureFlags = [.userInterface, .dataSyncing, .accountLogin, .accountCreation] + + /// Alias for the state when all features are available + public static let all: SyncFeatureFlags = .level3AllowCreateAccount + + // MARK: - + + init(privacyConfig: PrivacyConfiguration) { + guard privacyConfig.isEnabled(featureKey: .sync) else { + self = .unavailable + return + } + if !privacyConfig.isSubfeatureEnabled(SyncSubfeature.level0ShowSync) { + self = .unavailable + } else if !privacyConfig.isSubfeatureEnabled(SyncSubfeature.level1AllowDataSyncing) { + self = .level0ShowSync + } else if !privacyConfig.isSubfeatureEnabled(SyncSubfeature.level2AllowSetupFlows) { + self = .level1AllowDataSyncing + } else if !privacyConfig.isSubfeatureEnabled(SyncSubfeature.level3AllowCreateAccount) { + self = .level2AllowSetupFlows + } else { + self = .level3AllowCreateAccount + } + } +} diff --git a/Sources/DDGSync/internal/ProductionDependencies.swift b/Sources/DDGSync/internal/ProductionDependencies.swift index aff1764de..66724039e 100644 --- a/Sources/DDGSync/internal/ProductionDependencies.swift +++ b/Sources/DDGSync/internal/ProductionDependencies.swift @@ -16,8 +16,9 @@ // limitations under the License. // -import Foundation +import BrowserServicesKit import Common +import Foundation import Persistence struct ProductionDependencies: SyncDependencies { @@ -30,6 +31,7 @@ struct ProductionDependencies: SyncDependencies { let secureStore: SecureStoring let crypter: CryptingInternal let scheduler: SchedulingInternal + let privacyConfigurationManager: PrivacyConfigurationManaging let errorEvents: EventMapping var log: OSLog { @@ -37,12 +39,17 @@ struct ProductionDependencies: SyncDependencies { } private let getLog: () -> OSLog - init(serverEnvironment: ServerEnvironment, errorEvents: EventMapping, log: @escaping @autoclosure () -> OSLog = .disabled) { - + init( + serverEnvironment: ServerEnvironment, + privacyConfigurationManager: PrivacyConfigurationManaging, + errorEvents: EventMapping, + log: @escaping @autoclosure () -> OSLog = .disabled + ) { self.init(fileStorageUrl: FileManager.default.applicationSupportDirectoryForComponent(named: "Sync"), serverEnvironment: serverEnvironment, keyValueStore: UserDefaults(), secureStore: SecureStorage(), + privacyConfigurationManager: privacyConfigurationManager, errorEvents: errorEvents, log: log()) } @@ -52,6 +59,7 @@ struct ProductionDependencies: SyncDependencies { serverEnvironment: ServerEnvironment, keyValueStore: KeyValueStoring, secureStore: SecureStoring, + privacyConfigurationManager: PrivacyConfigurationManaging, errorEvents: EventMapping, log: @escaping @autoclosure () -> OSLog = .disabled ) { @@ -59,6 +67,7 @@ struct ProductionDependencies: SyncDependencies { self.endpoints = Endpoints(serverEnvironment: serverEnvironment) self.keyValueStore = keyValueStore self.secureStore = secureStore + self.privacyConfigurationManager = privacyConfigurationManager self.errorEvents = errorEvents self.getLog = log diff --git a/Sources/DDGSync/internal/SyncDependencies.swift b/Sources/DDGSync/internal/SyncDependencies.swift index d88f643f8..6e5740c0d 100644 --- a/Sources/DDGSync/internal/SyncDependencies.swift +++ b/Sources/DDGSync/internal/SyncDependencies.swift @@ -16,9 +16,10 @@ // limitations under the License. // -import Foundation +import BrowserServicesKit import Combine import Common +import Foundation import Persistence protocol SyncDependenciesDebuggingSupport { @@ -34,6 +35,7 @@ protocol SyncDependencies: SyncDependenciesDebuggingSupport { var secureStore: SecureStoring { get } var crypter: CryptingInternal { get } var scheduler: SchedulingInternal { get } + var privacyConfigurationManager: PrivacyConfigurationManaging { get } var errorEvents: EventMapping { get } var log: OSLog { get } diff --git a/Sources/DDGSync/internal/SyncQueue.swift b/Sources/DDGSync/internal/SyncQueue.swift index 9230a426b..a5a2bd4da 100644 --- a/Sources/DDGSync/internal/SyncQueue.swift +++ b/Sources/DDGSync/internal/SyncQueue.swift @@ -117,7 +117,22 @@ final class SyncQueue { } } + var isDataSyncingFeatureFlagEnabled: Bool = true { + didSet { + if isDataSyncingFeatureFlagEnabled { + os_log(.debug, log: self.log, "Sync Feature has been enabled") + } else { + os_log(.debug, log: self.log, "Sync Feature has been disabled, cancelling all operations") + operationQueue.cancelAllOperations() + } + } + } + func startSync() { + guard isDataSyncingFeatureFlagEnabled else { + os_log(.debug, log: self.log, "Sync Feature is temporarily disabled, not starting sync") + return + } let operation = makeSyncOperation() operationQueue.addOperation(operation) } diff --git a/Tests/DDGSyncTests/Mocks/Mocks.swift b/Tests/DDGSyncTests/Mocks/Mocks.swift index 08ea0168a..884956464 100644 --- a/Tests/DDGSyncTests/Mocks/Mocks.swift +++ b/Tests/DDGSyncTests/Mocks/Mocks.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import BrowserServicesKit import Combine import Common import Foundation @@ -132,6 +133,47 @@ class MockErrorHandler: EventMapping { } } +class MockPrivacyConfigurationManager: PrivacyConfigurationManaging { + var currentConfig: Data = .init() + var updatesSubject = PassthroughSubject() + let updatesPublisher: AnyPublisher + var privacyConfig: PrivacyConfiguration = MockPrivacyConfiguration() + func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { + .downloaded + } + + init() { + updatesPublisher = updatesSubject.eraseToAnyPublisher() + } +} + +class MockPrivacyConfiguration: PrivacyConfiguration { + + func isEnabled(featureKey: PrivacyFeature, versionProvider: AppVersionProvider) -> Bool { true } + + func isSubfeatureEnabled( + _ subfeature: any PrivacySubfeature, + versionProvider: AppVersionProvider, + randomizer: (Range) -> Double + ) -> Bool { + true + } + + var identifier: String = "abcd" + var userUnprotectedDomains: [String] = [] + var tempUnprotectedDomains: [String] = [] + var trackerAllowlist: PrivacyConfigurationData.TrackerAllowlist = .init(json: ["state": "disabled"])! + func exceptionsList(forFeature featureKey: PrivacyFeature) -> [String] { [] } + func isFeature(_ feature: PrivacyFeature, enabledForDomain: String?) -> Bool { true } + func isProtected(domain: String?) -> Bool { false } + func isUserUnprotected(domain: String?) -> Bool { false } + func isTempUnprotected(domain: String?) -> Bool { false } + func isInExceptionList(domain: String?, forFeature featureKey: PrivacyFeature) -> Bool { false } + func settings(for feature: PrivacyFeature) -> PrivacyConfigurationData.PrivacyFeature.FeatureSettings { .init() } + func userEnabledProtection(forDomain: String) {} + func userDisabledProtection(forDomain: String) {} +} + struct MockSyncDependencies: SyncDependencies, SyncDependenciesDebuggingSupport { var endpoints: Endpoints = Endpoints(baseURL: URL(string: "https://dev.null")!) var account: AccountManaging = AccountManagingMock() @@ -140,6 +182,7 @@ struct MockSyncDependencies: SyncDependencies, SyncDependenciesDebuggingSupport var crypter: CryptingInternal = CryptingMock() var scheduler: SchedulingInternal = SchedulerMock() var log: OSLog = .default + var privacyConfigurationManager: PrivacyConfigurationManaging = MockPrivacyConfigurationManager() var errorEvents: EventMapping = MockErrorHandler() var keyValueStore: KeyValueStoring = MockKeyValueStore() diff --git a/Tests/DDGSyncTests/SyncQueueTests.swift b/Tests/DDGSyncTests/SyncQueueTests.swift index 107b998da..66f530348 100644 --- a/Tests/DDGSyncTests/SyncQueueTests.swift +++ b/Tests/DDGSyncTests/SyncQueueTests.swift @@ -52,6 +52,31 @@ class SyncQueueTests: XCTestCase { requestMaker = SyncRequestMaker(storage: storage, api: apiMock, endpoints: endpoints) } + func testWhenDataSyncingFeatureFlagIsDisabledThenNewOperationsAreNotEnqueued() async { + let syncQueue = SyncQueue(dataProviders: [], storage: storage, crypter: crypter, api: apiMock, endpoints: endpoints) + XCTAssertFalse(syncQueue.operationQueue.isSuspended) + + var syncDidStartEvents = [Bool]() + let cancellable = syncQueue.isSyncInProgressPublisher.removeDuplicates().filter({ $0 }).sink { syncDidStartEvents.append($0) } + + syncQueue.isDataSyncingFeatureFlagEnabled = false + + await syncQueue.startSync() + await syncQueue.startSync() + await syncQueue.startSync() + + XCTAssertTrue(syncDidStartEvents.isEmpty) + + syncQueue.isDataSyncingFeatureFlagEnabled = true + + await syncQueue.startSync() + await syncQueue.startSync() + await syncQueue.startSync() + + cancellable.cancel() + XCTAssertEqual(syncDidStartEvents.count, 3) + } + func testThatInProgressPublisherEmitsValuesWhenSyncStartsAndEndsWithSuccess() async throws { let feature = Feature(name: "bookmarks") let dataProvider = DataProvidingMock(feature: feature) From 064ed560f19fbfd36eb02decebf944689818ed35 Mon Sep 17 00:00:00 2001 From: bwaresiak Date: Thu, 21 Dec 2023 16:47:39 +0100 Subject: [PATCH 38/39] Expose Internal User managing from Config (#610) Task/Issue URL: https://app.asana.com/0/414235014887631/1206217864822184/f --- .../PrivacyConfig/PrivacyConfigurationManager.swift | 3 ++- Sources/DDGSync/DDGSync.swift | 4 +++- Tests/DDGSyncTests/Mocks/Mocks.swift | 11 +++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationManager.swift b/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationManager.swift index a6ec94c66..406ed79f6 100644 --- a/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationManager.swift +++ b/Sources/BrowserServicesKit/PrivacyConfig/PrivacyConfigurationManager.swift @@ -31,6 +31,7 @@ public protocol PrivacyConfigurationManaging: AnyObject { var currentConfig: Data { get } var updatesPublisher: AnyPublisher { get } var privacyConfig: PrivacyConfiguration { get } + var internalUserDecider: InternalUserDecider { get } @discardableResult func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult } @@ -53,7 +54,7 @@ public class PrivacyConfigurationManager: PrivacyConfigurationManaging { private let embeddedDataProvider: EmbeddedDataProvider private let localProtection: DomainsProtectionStore private let errorReporting: EventMapping? - private let internalUserDecider: InternalUserDecider + public let internalUserDecider: InternalUserDecider private let installDate: Date? private let updatesSubject = PassthroughSubject() diff --git a/Sources/DDGSync/DDGSync.swift b/Sources/DDGSync/DDGSync.swift index 0df8e1e3f..179d6ac15 100644 --- a/Sources/DDGSync/DDGSync.swift +++ b/Sources/DDGSync/DDGSync.swift @@ -202,7 +202,9 @@ public class DDGSync: DDGSyncing { self.dataProvidersSource = dataProvidersSource self.dependencies = dependencies - featureFlagsCancellable = self.dependencies.privacyConfigurationManager.updatesPublisher + featureFlagsCancellable = Publishers.Merge( + self.dependencies.privacyConfigurationManager.updatesPublisher, + self.dependencies.privacyConfigurationManager.internalUserDecider.isInternalUserPublisher.map { _ in () }) .compactMap { [weak self] in self?.dependencies.privacyConfigurationManager.privacyConfig } diff --git a/Tests/DDGSyncTests/Mocks/Mocks.swift b/Tests/DDGSyncTests/Mocks/Mocks.swift index 884956464..2351cde6d 100644 --- a/Tests/DDGSyncTests/Mocks/Mocks.swift +++ b/Tests/DDGSyncTests/Mocks/Mocks.swift @@ -133,11 +133,22 @@ class MockErrorHandler: EventMapping { } } +final class MockInternalUserStoring: InternalUserStoring { + var isInternalUser: Bool = false +} + +extension DefaultInternalUserDecider { + convenience init(mockedStore: MockInternalUserStoring = MockInternalUserStoring()) { + self.init(store: mockedStore) + } +} + class MockPrivacyConfigurationManager: PrivacyConfigurationManaging { var currentConfig: Data = .init() var updatesSubject = PassthroughSubject() let updatesPublisher: AnyPublisher var privacyConfig: PrivacyConfiguration = MockPrivacyConfiguration() + let internalUserDecider: InternalUserDecider = DefaultInternalUserDecider() func reload(etag: String?, data: Data?) -> PrivacyConfigurationManager.ReloadResult { .downloaded } From ea133abe237b6cb57a4237e0373318a40c10afc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Thu, 21 Dec 2023 17:56:37 +0100 Subject: [PATCH 39/39] Fix privacy config fetch in debug mode (#606) --- Sources/Configuration/ConfigurationFetching.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/Configuration/ConfigurationFetching.swift b/Sources/Configuration/ConfigurationFetching.swift index bf214ccc0..26a7fb58f 100644 --- a/Sources/Configuration/ConfigurationFetching.swift +++ b/Sources/Configuration/ConfigurationFetching.swift @@ -22,7 +22,7 @@ import Networking protocol ConfigurationFetching { - func fetch(_ configuration: Configuration) async throws + func fetch(_ configuration: Configuration, isDebug: Bool) async throws func fetch(all configurations: [Configuration]) async throws } @@ -74,8 +74,9 @@ public final class ConfigurationFetcher: ConfigurationFetching { - Throws: An error of type Error is thrown if the configuration fails to fetch or validate. */ - public func fetch(_ configuration: Configuration) async throws { - let fetchResult = try await fetch(from: configuration.url, withEtag: etag(for: configuration), requirements: .default) + public func fetch(_ configuration: Configuration, isDebug: Bool = false) async throws { + let requirements: APIResponseRequirements = isDebug ? .requireNonEmptyData : .default + let fetchResult = try await fetch(from: configuration.url, withEtag: etag(for: configuration), requirements: requirements) if let data = fetchResult.data { try validator.validate(data, for: configuration) } @@ -134,7 +135,7 @@ public final class ConfigurationFetcher: ConfigurationFetching { let log = log let request = APIRequest(configuration: configuration, requirements: requirements, urlSession: urlSession, log: log) let (data, response) = try await request.fetch() - return (response.etag!, data) + return (response.etag ?? "", data) } private func store(_ result: ConfigurationFetchResult, for configuration: Configuration) throws {