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 000000000..dbfdd2175 Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite differ diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-shm b/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-shm new file mode 100644 index 000000000..0dd5f7f20 Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-shm differ diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-wal b/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-wal new file mode 100644 index 000000000..aff9e2d4d Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V1.sqlite-wal differ diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite b/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite new file mode 100644 index 000000000..3c2b9e75c Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite differ diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-shm b/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-shm new file mode 100644 index 000000000..60729980b Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-shm differ diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-wal b/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-wal new file mode 100644 index 000000000..17e45ca31 Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V2.sqlite-wal differ diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite b/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite new file mode 100644 index 000000000..494c6d240 Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite differ diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-shm b/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-shm new file mode 100644 index 000000000..0d1100a08 Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-shm differ diff --git a/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-wal b/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-wal new file mode 100644 index 000000000..e7fdc69ee Binary files /dev/null and b/Tests/BookmarksTests/Resources/Bookmarks_V3.sqlite-wal differ