/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import Foundation import Shared @testable import Storage import XCTest private func getBrowserDB(_ filename: String, files: FileAccessor) -> BrowserDB? { return BrowserDB(filename: filename, schema: BrowserSchema(), files: files) } extension SQLiteBookmarks { var testFactory: SQLiteBookmarksModelFactory { return SQLiteBookmarksModelFactory(bookmarks: self, direction: .local) } } // MARK: - Tests. class TestTruncation: XCTestCase { func testTruncate() { let a = "🤠" let b = "abcdefghi" let c = "" XCTAssertEqual(a.truncateToUTF8ByteCount(1), "") XCTAssertEqual(a.truncateToUTF8ByteCount(4), a) XCTAssertEqual(a.truncateToUTF8ByteCount(16), a) XCTAssertEqual(b.truncateToUTF8ByteCount(16), b) XCTAssertEqual(b.truncateToUTF8ByteCount(5), "abcde") XCTAssertEqual(b.truncateToUTF8ByteCount(0), "") XCTAssertEqual(c.truncateToUTF8ByteCount(1), c) XCTAssertEqual(c.truncateToUTF8ByteCount(4), c) } } class BrowserDBV15: Schema { var name: String = "BROWSER" var version: Int = 15 func getHistoryTableCreationString() -> String { return "CREATE TABLE IF NOT EXISTS history (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "guid TEXT NOT NULL UNIQUE, " + // Not null, but the value might be replaced by the server's. "url TEXT UNIQUE, " + // May only be null for deleted records. "title TEXT NOT NULL, " + "server_modified INTEGER, " + // Can be null. Integer milliseconds. "local_modified INTEGER, " + // Can be null. Client clock. In extremis only. "is_deleted TINYINT NOT NULL, " + // Boolean. Locally deleted. "should_upload TINYINT NOT NULL, " + // Boolean. Set when changed or visits added. "domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE, " + "CONSTRAINT urlOrDeleted CHECK (url IS NOT NULL OR is_deleted = 1)" + ")" } func getDomainsTableCreationString() -> String { return "CREATE TABLE IF NOT EXISTS \(TableDomains) (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "domain TEXT NOT NULL UNIQUE, " + "showOnTopSites TINYINT NOT NULL DEFAULT 1" + ")" } func getQueueTableCreationString() -> String { return "CREATE TABLE IF NOT EXISTS \(TableQueuedTabs) (" + "url TEXT NOT NULL UNIQUE, " + "title TEXT" + ") " } func getVisitsTableCreationString() -> String { return "CREATE TABLE IF NOT EXISTS \(TableVisits) (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "siteID INTEGER NOT NULL REFERENCES history(id) ON DELETE CASCADE, " + "date REAL NOT NULL, " + // Microseconds since epoch. "type INTEGER NOT NULL, " + "is_local TINYINT NOT NULL, " + // Some visits are local. Some are remote ('mirrored'). This boolean flag is the split. "UNIQUE (siteID, date, type) " + ") " } func getFaviconsTableCreationString() -> String { return "CREATE TABLE IF NOT EXISTS favicons (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "url TEXT NOT NULL UNIQUE, " + "width INTEGER, " + "height INTEGER, " + "type INTEGER NOT NULL, " + "date REAL NOT NULL" + ") " } func getBookmarksTableCreationStringForTable(_ table: String, withAdditionalColumns: String="") -> String { // The stupid absence of naming conventions here is thanks to pre-Sync Weave. Sorry. // For now we have the simplest possible schema: everything in one. let sql = "CREATE TABLE IF NOT EXISTS \(table) " + // Shared fields. "( id INTEGER PRIMARY KEY AUTOINCREMENT" + ", guid TEXT NOT NULL UNIQUE" + ", type TINYINT NOT NULL" + // Type enum. // Record/envelope metadata that'll allow us to do merges. ", is_deleted TINYINT NOT NULL DEFAULT 0" + // Boolean ", parentid TEXT" + // GUID ", parentName TEXT" + // Type-specific fields. These should be NOT NULL in many cases, but we're going // for a sparse schema, so this'll do for now. Enforce these in the application code. ", feedUri TEXT, siteUri TEXT" + // LIVEMARKS ", pos INT" + // SEPARATORS ", title TEXT, description TEXT" + // FOLDERS, BOOKMARKS, QUERIES ", bmkUri TEXT, tags TEXT, keyword TEXT" + // BOOKMARKS, QUERIES ", folderName TEXT, queryId TEXT" + // QUERIES withAdditionalColumns + ", CONSTRAINT parentidOrDeleted CHECK (parentid IS NOT NULL OR is_deleted = 1)" + ", CONSTRAINT parentNameOrDeleted CHECK (parentName IS NOT NULL OR is_deleted = 1)" + ")" return sql } func getBookmarksStructureTableCreationStringForTable(_ table: String, referencingMirror mirror: String) -> String { let sql = "CREATE TABLE IF NOT EXISTS \(table) " + "( parent TEXT NOT NULL REFERENCES \(mirror)(guid) ON DELETE CASCADE" + ", child TEXT NOT NULL" + // Should be the GUID of a child. ", idx INTEGER NOT NULL" + // Should advance from 0. ")" return sql } let iconColumns = ", faviconID INTEGER REFERENCES \(TableFavicons)(id) ON DELETE SET NULL" let mirrorColumns = ", is_overridden TINYINT NOT NULL DEFAULT 0" let serverColumns = ", server_modified INTEGER NOT NULL" + // Milliseconds. ", hasDupe TINYINT NOT NULL DEFAULT 0" // Boolean, 0 (false) if deleted. let localColumns = ", local_modified INTEGER" + // Can be null. Client clock. In extremis only. ", sync_status TINYINT NOT NULL" // SyncStatus enum. Set when changed or created. func create(_ db: SQLiteDBConnection) -> Bool { let bookmarksLocal = self.getBookmarksTableCreationStringForTable(TableBookmarksLocal, withAdditionalColumns: self.localColumns + self.iconColumns) let bookmarksLocalStructure = self.getBookmarksStructureTableCreationStringForTable(TableBookmarksLocalStructure, referencingMirror: TableBookmarksLocal) let bookmarksBuffer = getBookmarksTableCreationStringForTable(TableBookmarksBuffer, withAdditionalColumns: self.serverColumns) let bookmarksBufferStructure = self.getBookmarksStructureTableCreationStringForTable(TableBookmarksBufferStructure, referencingMirror: TableBookmarksBuffer) let bookmarksMirror = getBookmarksTableCreationStringForTable(TableBookmarksMirror, withAdditionalColumns: self.serverColumns + self.mirrorColumns + self.iconColumns) let bookmarksMirrorStructure = self.getBookmarksStructureTableCreationStringForTable(TableBookmarksMirrorStructure, referencingMirror: TableBookmarksMirror) let stmts = [ bookmarksLocal, bookmarksLocalStructure, bookmarksBuffer, bookmarksBufferStructure, bookmarksMirror, bookmarksMirrorStructure, self.getHistoryTableCreationString(), self.getDomainsTableCreationString(), self.getQueueTableCreationString(), self.getVisitsTableCreationString(), self.getFaviconsTableCreationString() ] do { for sql in stmts { try db.executeChange(sql, withArgs: nil) } } catch _ as NSError { return false } return true } func update(_ db: SQLiteDBConnection, from: Int) -> Bool { // Do nothing return false } func drop(_ db: SQLiteDBConnection) -> Bool { // Do nothing return false } } class TestSQLiteBookmarks: XCTestCase { let files = MockFiles() fileprivate func remove(_ path: String) { do { try self.files.remove(path) } catch {} } override func tearDown() { self.remove("TSQLBtestBookmarks.db") self.remove("TSQLBtestBufferStorage.db") self.remove("TSQLBtestLocalAndMirror.db") self.remove("TSQLBtestRecursiveAndURLDelete.db") self.remove("TSQLBtestUnrooted.db") self.remove("TSQLBtestTreeBuilding.db") self.remove("TSQLBtestLocalBookmarksModifications.db") self.remove("TSQLBtestApplyBufferUpdatedCompletionOp.db") self.remove("TSQLBtestApplyRecordsPendingDeletions.db") self.remove("TSQLBtestDBUpgradeBeforeDateAdded.db") super.tearDown() } func testDateAddedMigration() { let schema = BrowserDBV15() let destination = BrowserSchema() var db = BrowserDB(filename: "TSQLBtestDBUpgradeBeforeDateAdded.db", schema: schema, files: files) XCTAssertTrue(db.withConnection({ connection -> Int in connection.version }).value.successValue == schema.version, "Creating BrowserSchema beforeDateAdded") db.forceClose() db = BrowserDB(filename: "TSQLBtestDBUpgradeBeforeDateAdded.db", schema: destination, files: files) XCTAssertTrue(db.withConnection({ connection -> Int in connection.version }).value.successValue == destination.version, "Upgrading BrowserSchema to the latest version") db.forceClose() } func testBookmarks() { guard let db = getBrowserDB("TSQLBtestBookmarks.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = SQLiteBookmarks(db: db) let factory = bookmarks.testFactory let url = "http://url1/" let u = url.asURL! bookmarks.addToMobileBookmarks(u, title: "Title", favicon: nil).succeeded() let model = factory.modelForFolder(BookmarkRoots.MobileFolderGUID).value.successValue XCTAssertEqual((model?.current[0] as? BookmarkItem)?.url, url) XCTAssertTrue(factory.isBookmarked(url).value.successValue ?? false) factory.removeByURL("").succeeded() // Grab that GUID and move it into desktop bookmarks. let guid = (model?.current[0] as! BookmarkItem).guid // Desktop bookmarks. XCTAssertFalse(factory.hasDesktopBookmarks().value.successValue ?? true) let toolbar = BookmarkRoots.ToolbarFolderGUID XCTAssertTrue(bookmarks.db.run([ "UPDATE \(TableBookmarksLocal) SET parentid = '\(toolbar)' WHERE guid = '\(guid)'", "UPDATE \(TableBookmarksLocalStructure) SET parent = '\(toolbar)' WHERE child = '\(guid)'", ]).value.isSuccess) XCTAssertTrue(factory.hasDesktopBookmarks().value.successValue ?? true) } func testGetLocalBookmarksModifications() { guard let db = getBrowserDB("TSQLBtestLocalBookmarksModifications.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = SQLiteBookmarks(db: db) let localQuery = "INSERT INTO \(TableBookmarksLocal) (guid, type, bmkUri, title, parentid, parentName, sync_status) " + "VALUES " + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', 2), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 0), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', 2) " let localArgs: Args = [ "folder123", "123 (nok)", BookmarkRoots.MobileFolderGUID, "bookmark123", "http://example.org/1", "Bookmark in folder 123 (nok)", "folder123", "bookmark_other_folder", "http://example.org/2", "Bookmark in another folder (nok)", BookmarkRoots.ToolbarFolderGUID, "bookmark_good_nonsynced", "http://example.org/3", "Bookmark 1 (ok)", BookmarkRoots.MobileFolderGUID, "bookmark_good_synced", "http://example.org/4", "Bookmark 2 (nok)", BookmarkRoots.MobileFolderGUID, "bookmark_duplicate_in_buffer", "http://example.org/5", "Bookmark in buffer(nok)", BookmarkRoots.MobileFolderGUID, "bookmark_good_additional", "http://example.org/6", "Bookmark additional 1", BookmarkRoots.MobileFolderGUID, "bookmark_good_additional_over_limit", "http://example.org/7", "Bookmark additional 2", BookmarkRoots.MobileFolderGUID, ] let structureQuery = "INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) VALUES " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?) " let structureArgs: Args = [ BookmarkRoots.MobileFolderGUID, "folder123", 0, "folder123", "bookmark123", 0, BookmarkRoots.ToolbarFolderGUID, "bookmark_other_folder", 0, BookmarkRoots.MobileFolderGUID, "bookmark_good_nonsynced", 1, BookmarkRoots.MobileFolderGUID, "bookmark_good_synced", 2, BookmarkRoots.MobileFolderGUID, "bookmark_duplicate_in_buffer", 3, BookmarkRoots.MobileFolderGUID, "bookmark_good_additional", 4, BookmarkRoots.MobileFolderGUID, "bookmark_good_additional_over_limit", 5, ] let bufferQuery = "INSERT INTO \(TableBookmarksBuffer) (guid, type, bmkUri, title, parentid, parentName, server_modified) VALUES " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?) " let bufferArgs: Args = [ "bookmark_duplicate_in_buffer", "http://example.org/5", "Bookmark in buffer(nok)", BookmarkRoots.MobileFolderGUID, Date.now(), "bookmark_to_delete", "http://example.org/delme", "Bookmark in buffer to delete(ok)", BookmarkRoots.MobileFolderGUID, 88888 ] let bufferStructureQuery = "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES " + "(?, ?, ?), " + "(?, ?, ?)" let bufferStructureArgs: Args = [ "bookmark_duplicate_in_buffer", "bookmark_duplicate_in_buffer", 0, // It's its own parent because we are lazy. "bookmark_to_delete", "bookmark_to_delete", 0 ] let pendingDeletionsQuery = "INSERT INTO \(TablePendingBookmarksDeletions) (id) VALUES " + "(?)" let pendingDeletionsArgs: Args = [ "bookmark_to_delete" ] db.run([ (sql: localQuery, args: localArgs), (sql: structureQuery, args: structureArgs), (sql: bufferQuery, args: bufferArgs), (sql: bufferStructureQuery, args: bufferStructureArgs), (sql: pendingDeletionsQuery, args: pendingDeletionsArgs), ]).succeeded() var modifications = bookmarks.getLocalBookmarksModifications(limit: 3).value.successValue! XCTAssertEqual(modifications.additions.count, 2) XCTAssertEqual(modifications.additions.map { $0.guid }, ["bookmark_good_nonsynced", "bookmark_good_additional"]) XCTAssertEqual(modifications.deletions.count, 1) XCTAssertEqual(modifications.deletions, ["bookmark_to_delete"]) // Deletions are prioritized. modifications = bookmarks.getLocalBookmarksModifications(limit: 1).value.successValue! XCTAssertEqual(modifications.additions.count, 0) XCTAssertEqual(modifications.deletions.count, 1) XCTAssertEqual(modifications.deletions, ["bookmark_to_delete"]) } func testApplyRecordsRemovesPendingDeletions() { guard let db = getBrowserDB("TSQLBtestApplyRecordsPendingDeletions.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = MergedSQLiteBookmarks(db: db) let bufferQuery = "INSERT INTO \(TableBookmarksBuffer) (guid, type, bmkUri, title, parentid, parentName, server_modified) VALUES " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?) " let bufferArgs: Args = [ "bkm1", "http://example.org/1", "Bookmark 1", BookmarkRoots.MobileFolderGUID, Date.now(), "bkm2", "http://example.org/2", "Bookmark 2", BookmarkRoots.MobileFolderGUID, Date.now() ] let bufferStructureQuery = "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES " + "(?, ?, ?), " + "(?, ?, ?)" let bufferStructureArgs: Args = [ "bkm1", "bkm1", 0, // It's its own parent because we are lazy. "bkm2", "bkm2", 0 ] let pendingDeletionsQuery = "INSERT INTO \(TablePendingBookmarksDeletions) (id) VALUES " + "(?)" let pendingDeletionsArgs: Args = [ "bkm2" ] db.run([ (sql: bufferQuery, args: bufferArgs), (sql: bufferStructureQuery, args: bufferStructureArgs), (sql: pendingDeletionsQuery, args: pendingDeletionsArgs), ]).succeeded() let modified: [BookmarkMirrorItem] = [BookmarkMirrorItem.bookmark("bkm2", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: "bkm2", parentName: nil, title: "BKM 2", description: nil, URI: "https://test.com", tags: "", keyword: nil)] bookmarks.applyRecords(modified).succeeded() XCTAssertTrue(db.queryReturnsNoResults("SELECT * FROM \(TablePendingBookmarksDeletions)").value.successValue!) } func testApplyBufferUpdatedCompletionOp() { guard let db = getBrowserDB("TSQLBtestApplyBufferUpdatedCompletionOp.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = SQLiteBookmarks(db: db) bookmarks.addToMobileBookmarks("http://example.org/1".asURL!, title: "Bookmark 1", favicon: nil).succeeded() bookmarks.addToMobileBookmarks("http://example.org/2".asURL!, title: "Bookmark 2", favicon: nil).succeeded() bookmarks.addToMobileBookmarks("http://example.org/3".asURL!, title: "Bookmark 3", favicon: nil).succeeded() bookmarks.addToMobileBookmarks("http://example.org/4".asURL!, title: "Bookmark 4", favicon: nil).succeeded() var localTree = bookmarks.treeForLocal().value.successValue! var mobileFolderNode = localTree.find(BookmarkRoots.MobileFolderGUID)! let localChildrenGUIDs = mobileFolderNode.children!.map { $0.recordGUID } XCTAssertEqual(localChildrenGUIDs.count, 4) let childrenGUIDsUploaded = localChildrenGUIDs.dropLast(1) let childrenGUIDsFailed = localChildrenGUIDs.dropFirst(3) let bufferQuery = "INSERT INTO \(TableBookmarksBuffer) (guid, type, bmkUri, title, parentid, parentName, server_modified) VALUES " + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', ?), " + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', ?), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', ?) " let bkmBufModified = Date.now() let bufferArgs: Args = [ BookmarkRoots.RootGUID, "", BookmarkRoots.RootGUID, 123, BookmarkRoots.MobileFolderGUID, "Mobile Bookmarks", BookmarkRoots.RootGUID, 456, "bkmbuf", "http://example.org/5", "Bookmark 5", BookmarkRoots.MobileFolderGUID, bkmBufModified, "bkmtodelete", "http://example.org/to_delete", "Bookmark to delete", BookmarkRoots.MobileFolderGUID, 88888, "bkmtodelete_uploadfail", "http://example.org/to_delete_updfail", "Bookmark to delete but upload failed", BookmarkRoots.MobileFolderGUID, 88888 ] let bufferStructureQuery = "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?)" let bufferStructureArgs: Args = [ BookmarkRoots.RootGUID, BookmarkRoots.RootGUID, 0, BookmarkRoots.RootGUID, BookmarkRoots.MobileFolderGUID, 0, BookmarkRoots.MobileFolderGUID, "bkmbuf", 0, BookmarkRoots.MobileFolderGUID, "bkmtodelete", 1, BookmarkRoots.MobileFolderGUID, "bkmtodelete_uploadfail", 2, ] let pendingDeletionsQuery = "INSERT INTO \(TablePendingBookmarksDeletions) (id) VALUES " + "(?), " + "(?)" let pendingDeletionsArgs: Args = [ "bkmtodelete", "bkmtodelete_uploadfail" ] db.run([ (sql: bufferQuery, args: bufferArgs), (sql: bufferStructureQuery, args: bufferStructureArgs), (sql: pendingDeletionsQuery, args: pendingDeletionsArgs), ]).succeeded() let mobileRoot = BookmarkMirrorItem.folder(BookmarkRoots.MobileFolderGUID, dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.MobileFolderGUID, parentName: nil, title: "Mobile Bookmarks", description: nil, children: ["bkmbuf"] + childrenGUIDsUploaded) let op = BufferUpdatedCompletionOp(bufferValuesToMoveFromLocal: Set(childrenGUIDsUploaded), deletedValues: Set(["bkmtodelete"]), mobileRoot: mobileRoot, modifiedTime: 123456) let mergedBookmarks = MergedSQLiteBookmarks(db: db) mergedBookmarks.applyBufferUpdatedCompletionOp(op).succeeded() let rootFolder = mergedBookmarks.getBufferItemWithGUID(BookmarkRoots.RootGUID).value.successValue! XCTAssertEqual(rootFolder.serverModified, 123) let mobileFolder = mergedBookmarks.getBufferItemWithGUID(BookmarkRoots.MobileFolderGUID).value.successValue! XCTAssertEqual(mobileFolder.serverModified, 123456) let childrenGUIDs = mergedBookmarks.getBufferChildrenGUIDsForParent(BookmarkRoots.MobileFolderGUID).value.successValue! XCTAssertEqual(childrenGUIDs, ["bkmbuf", "bkmtodelete_uploadfail"] + childrenGUIDsUploaded) let children = mergedBookmarks.getBufferItemsWithGUIDs(["bkmbuf"] + childrenGUIDsUploaded).value.successValue! for item in children.values { XCTAssertEqual(item.serverModified, item.guid == "bkmbuf" ? bkmBufModified : 123456) } localTree = bookmarks.treeForLocal().value.successValue! mobileFolderNode = localTree.find(BookmarkRoots.MobileFolderGUID)! XCTAssertEqual(mobileFolderNode.children!.map { $0.recordGUID }, Array(childrenGUIDsFailed)) } fileprivate func createStockMirrorTree(_ db: BrowserDB) { // Set up a mirror tree. let mirrorQuery = "INSERT INTO \(TableBookmarksMirror) (guid, type, bmkUri, title, parentid, parentName, description, tags, keyword, is_overridden, server_modified, pos) " + "VALUES " + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + "(?, \(BookmarkNodeType.folder.rawValue), NULL, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + "(?, \(BookmarkNodeType.separator.rawValue), NULL, NULL, ?, '', '', '', '', 0, \(Date.now()), 0), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL), " + "(?, \(BookmarkNodeType.bookmark.rawValue), ?, ?, ?, '', '', '', '', 0, \(Date.now()), NULL) " let mirrorArgs: Args = [ "folderAAAAAA", "AAA", BookmarkRoots.ToolbarFolderGUID, "folderBBBBBB", "BBB", BookmarkRoots.MenuFolderGUID, "folderCCCCCC", "CCC", "folderBBBBBB", "separator101", "folderAAAAAA", "bookmark1001", "http://example.org/1", "Bookmark 1", "folderAAAAAA", "bookmark1002", "http://example.org/1", "Bookmark 1 Again", "folderAAAAAA", "bookmark2001", "http://example.org/2", "Bookmark 2", "folderAAAAAA", "bookmark2002", "http://example.org/2", "Bookmark 2 Again", "folderCCCCCC", "bookmark3001", "http://example.org/3", "Bookmark 3", "folderBBBBBB", ] let structureQuery = "INSERT INTO \(TableBookmarksMirrorStructure) (parent, child, idx) VALUES " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?), " + "(?, ?, ?) " let structureArgs: Args = [ BookmarkRoots.ToolbarFolderGUID, "folderAAAAAA", 0, BookmarkRoots.MenuFolderGUID, "folderBBBBBB", 0, "folderAAAAAA", "bookmark1001", 0, "folderAAAAAA", "separator101", 1, "folderAAAAAA", "bookmark1002", 2, "folderAAAAAA", "bookmark2001", 3, "folderBBBBBB", "bookmark3001", 0, "folderBBBBBB", "folderCCCCCC", 1, "folderCCCCCC", "bookmark2002", 0, ] db.moveLocalToMirrorForTesting() // So we have the roots. db.run([ (sql: mirrorQuery, args: mirrorArgs), (sql: structureQuery, args: structureArgs), ]).succeeded() } fileprivate func isUnknown(_ folder: BookmarkTreeNode, withGUID: GUID) { switch folder { case .unknown(let guid): XCTAssertEqual(withGUID, guid) default: XCTFail("Not an unknown with GUID \(withGUID).") } } fileprivate func isNonFolder(_ folder: BookmarkTreeNode, withGUID: GUID) { switch folder { case .nonFolder(let guid): XCTAssertEqual(withGUID, guid) default: XCTFail("Not a non-folder with GUID \(withGUID).") } } fileprivate func isFolder(_ folder: BookmarkTreeNode, withGUID: GUID) { switch folder { case .folder(let record): XCTAssertEqual(withGUID, record.guid) default: XCTFail("Not a folder with GUID \(withGUID).") } } fileprivate func areFolders(_ folders: [BookmarkTreeNode], withGUIDs: [GUID]) { folders.zip(withGUIDs).forEach { (node, guid) in self.isFolder(node, withGUID: guid) } } fileprivate func assertTreeIsEmpty(_ treeMaybe: Maybe) { guard let tree = treeMaybe.successValue else { XCTFail("Couldn't get tree!") return } XCTAssertTrue(tree.orphans.isEmpty) XCTAssertTrue(tree.deleted.isEmpty) XCTAssertTrue(tree.isEmpty) } fileprivate func assertTreeContainsOnlyRoots(_ treeMaybe: Maybe) { guard let tree = treeMaybe.successValue else { XCTFail("Couldn't get tree!") return } XCTAssertTrue(tree.orphans.isEmpty) XCTAssertTrue(tree.deleted.isEmpty) XCTAssertFalse(tree.isEmpty) XCTAssertEqual(1, tree.subtrees.count) if case let .folder(guid, children) = tree.subtrees[0] { XCTAssertEqual(guid, "root________") XCTAssertEqual(4, children.count) children.forEach { child in guard case let .folder(_, lower) = child, lower.isEmpty else { XCTFail("Child \(child) wasn't empty!") return } } } else { XCTFail("Tree didn't contain root.") } } func testUnrootedBufferRowsDontAppearInTrees() { guard let db = getBrowserDB("TSQLBtestUnrooted.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = SQLiteBookmarks(db: db) self.assertTreeContainsOnlyRoots(bookmarks.treeForMirror().value) self.assertTreeIsEmpty(bookmarks.treeForBuffer().value) self.assertTreeContainsOnlyRoots(bookmarks.treeForLocal().value) let args: Args = [ "unrooted0001", BookmarkNodeType.bookmark.rawValue, 0, "somefolder01", "Some Folder", "I have no folder", "http://example.org/", "rooted000002", BookmarkNodeType.bookmark.rawValue, 0, "somefolder02", "Some Other Folder", "I have a folder", "http://example.org/", "somefolder02", BookmarkNodeType.folder.rawValue, 0, BookmarkRoots.MobileFolderGUID, "Mobile Bookmarks", "Some Other Folder", ] let now = Date.now() let bufferSQL = "INSERT INTO \(TableBookmarksBuffer) (server_modified, guid, type, date_added, is_deleted, parentid, parentName, title, bmkUri) VALUES " + "(\(now), ?, ?, \(now), ?, ?, ?, ?, ?), " + "(\(now), ?, ?, \(now), ?, ?, ?, ?, ?), " + "(\(now), ?, ?, \(now), ?, ?, ?, ?, NULL)" let bufferStructureSQL = "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES ('somefolder02', 'rooted000002', 0)" db.run(bufferSQL, withArgs: args).succeeded() db.run(bufferStructureSQL).succeeded() let tree = bookmarks.treeForBuffer().value.successValue! XCTAssertFalse(tree.orphans.contains("somefolder02")) // Folders are never orphans; they appear in subtrees instead. XCTAssertFalse(tree.orphans.contains("rooted000002")) // This tree contains its parent, so it's not an orphan. XCTAssertTrue(tree.orphans.contains("unrooted0001")) XCTAssertEqual(Set(tree.subtrees.map { $0.recordGUID }), Set(["somefolder02"])) } func testTreeBuilding() { guard let db = getBrowserDB("TSQLBtestTreeBuilding.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = SQLiteBookmarks(db: db) self.assertTreeContainsOnlyRoots(bookmarks.treeForMirror().value) self.assertTreeIsEmpty(bookmarks.treeForBuffer().value) self.assertTreeContainsOnlyRoots(bookmarks.treeForLocal().value) self.createStockMirrorTree(db) self.assertTreeIsEmpty(bookmarks.treeForBuffer().value) // Local was emptied when we moved the roots to the mirror. self.assertTreeIsEmpty(bookmarks.treeForLocal().value) guard let tree = bookmarks.treeForMirror().value.successValue else { XCTFail("Couldn't get tree!") return } // Mirror is no longer empty. XCTAssertFalse(tree.isEmpty) // There's one root. XCTAssertEqual(1, tree.subtrees.count) if case let .folder(guid, children) = tree.subtrees[0] { XCTAssertEqual("root________", guid) XCTAssertEqual(4, children.count) self.areFolders(children, withGUIDs: BookmarkRoots.RootChildren) } else { XCTFail("Root should be a folder.") } // Every GUID is in the tree's nodes. ["folderAAAAAA", "folderBBBBBB", "folderCCCCCC"].forEach { isFolder(tree.lookup[$0]!, withGUID: $0) } ["separator101", "bookmark1001", "bookmark1002", "bookmark2001", "bookmark2002", "bookmark3001"].forEach { isNonFolder(tree.lookup[$0]!, withGUID: $0) } let expectedCount = BookmarkRoots.RootChildren.count + 1 + // The roots. 6 + // Non-folders. 3 // Folders. XCTAssertEqual(expectedCount, tree.lookup.count) // There are no orphans and no deletions. XCTAssertTrue(tree.orphans.isEmpty) XCTAssertTrue(tree.deleted.isEmpty) // root________ // menu________ // folderBBBBBB // bookmark3001 if case let .folder(guidR, rootChildren) = tree.subtrees[0] { XCTAssertEqual(guidR, "root________") if case let .folder(guidM, menuChildren) = rootChildren[0] { XCTAssertEqual(guidM, "menu________") if case let .folder(guidB, bbbChildren) = menuChildren[0] { XCTAssertEqual(guidB, "folderBBBBBB") // BBB contains bookmark3001. if case let .nonFolder(guidBM) = bbbChildren[0] { XCTAssertEqual(guidBM, "bookmark3001") } else { XCTFail("First child of BBB should be bookmark3001.") } // BBB contains folderCCCCCC. if case let .folder(guidBF, _) = bbbChildren[1] { XCTAssertEqual(guidBF, "folderCCCCCC") } else { XCTFail("Second child of BBB should be folderCCCCCC.") } } else { XCTFail("First child of menu should be BBB.") } } else { XCTFail("First child of root should be menu________") } } else { XCTFail("First root should be root________") } // Add a bookmark. It'll override the folder. bookmarks.insertBookmark("https://foo.com/".asURL!, title: "Foo", favicon: nil, intoFolder: "folderBBBBBB", withTitle: "BBB").succeeded() let newlyInserted = db.getRecordByURL("https://foo.com/", fromTable: TableBookmarksLocal).guid guard let local = bookmarks.treeForLocal().value.successValue else { XCTFail("Couldn't get local tree!") return } XCTAssertFalse(local.isEmpty) XCTAssertEqual(4, local.lookup.count) // Folder, new bookmark, original two children. XCTAssertEqual(1, local.subtrees.count) if case let .folder(guid, children) = local.subtrees[0] { XCTAssertEqual("folderBBBBBB", guid) // We have shadows of the original two children. XCTAssertEqual(3, children.count) self.isUnknown(children[0], withGUID: "bookmark3001") self.isUnknown(children[1], withGUID: "folderCCCCCC") self.isNonFolder(children[2], withGUID: newlyInserted) } else { XCTFail("Root should be folderBBBBBB.") } // Insert partial data into the buffer. // We insert: let bufferArgs: Args = [ // * A folder whose parent isn't present in the structure. "ihavenoparent", BookmarkNodeType.folder.rawValue, 0, "myparentnoexist", "No Exist", "No Parent", // * A folder with no children. "ihavenochildren", BookmarkNodeType.folder.rawValue, 0, "ihavenoparent", "No Parent", "No Children", // * A folder that meets both criteria. "xhavenoparent", BookmarkNodeType.folder.rawValue, 0, "myparentnoexist", "No Exist", "No Parent And No Children", // * A changed bookmark with no parent. "changedbookmark", BookmarkNodeType.bookmark.rawValue, 0, "folderCCCCCC", "CCC", "I changed", "http://change.org/", // * A deleted record. "iwasdeleted", BookmarkNodeType.bookmark.rawValue, ] let now = Date.now() let bufferSQL = "INSERT INTO \(TableBookmarksBuffer) (server_modified, guid, type, date_added, is_deleted, parentid, parentName, title, bmkUri) VALUES " + "(\(now), ?, ?, \(now), ?, ?, ?, ?, NULL), " + "(\(now), ?, ?, \(now), ?, ?, ?, ?, NULL), " + "(\(now), ?, ?, \(now), ?, ?, ?, ?, NULL), " + "(\(now), ?, ?, \(now), ?, ?, ?, ?, ?), " + "(\(now), ?, ?, \(now), 1, NULL, NULL, NULL, NULL) " let bufferStructureSQL = "INSERT INTO \(TableBookmarksBufferStructure) (parent, child, idx) VALUES (?, ?, ?)" let bufferStructureArgs: Args = ["ihavenoparent", "ihavenochildren", 0] db.run([(bufferSQL, bufferArgs), (bufferStructureSQL, bufferStructureArgs)]).succeeded() // Now build the tree. guard let partialBuffer = bookmarks.treeForBuffer().value.successValue else { XCTFail("Couldn't get buffer tree!") return } XCTAssertEqual(partialBuffer.deleted, Set(["iwasdeleted"])) XCTAssertEqual(partialBuffer.orphans, Set(["changedbookmark"])) XCTAssertEqual(partialBuffer.subtreeGUIDs, Set(["ihavenoparent", "xhavenoparent"])) if case let .folder(_, children) = partialBuffer.lookup["ihavenochildren"]! { XCTAssertTrue(children.isEmpty) } else { XCTFail("Couldn't look up childless folder.") } } func testRecursiveAndURLDelete() { guard let db = getBrowserDB("TSQLBtestRecursiveAndURLDelete.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = SQLiteBookmarks(db: db) self.createStockMirrorTree(db) let menuOverridden = BookmarkRoots.MenuFolderGUID XCTAssertFalse(db.isOverridden(menuOverridden) ?? true) func getMenuChildren() -> [GUID] { return db.getChildrenOfFolder(BookmarkRoots.MenuFolderGUID) } XCTAssertEqual(["folderBBBBBB"], getMenuChildren()) // Locally add an item to the menu. This'll override the menu folder. bookmarks.insertBookmark(URL(string: "http://example.com/2")!, title: "Bookmark 2 added locally", favicon: nil, intoFolder: BookmarkRoots.MenuFolderGUID, withTitle: "Bookmarks Menu").succeeded() XCTAssertTrue(db.isOverridden(BookmarkRoots.MenuFolderGUID) ?? false) let menuChildrenBeforeRecursiveDelete = getMenuChildren() XCTAssertEqual(2, menuChildrenBeforeRecursiveDelete.count) XCTAssertEqual("folderBBBBBB", menuChildrenBeforeRecursiveDelete[0]) let locallyAddedBookmark2 = menuChildrenBeforeRecursiveDelete[1] // It's local, so it's not overridden. // N.B., these tests all use XCTAssertEqual instead of XCTAssert{True,False} because // the former plays well with optionals. XCTAssertNil(db.isOverridden(locallyAddedBookmark2)) XCTAssertEqual(false, db.isLocallyDeleted(locallyAddedBookmark2)) // Now let's delete folder B and check that things that weren't deleted now are. XCTAssertEqual(false, db.isOverridden("folderBBBBBB")) XCTAssertNil(db.isLocallyDeleted("folderBBBBBB")) XCTAssertEqual(false, db.isOverridden("folderCCCCCC")) XCTAssertNil(db.isLocallyDeleted("folderCCCCCC")) XCTAssertEqual(false, db.isOverridden("bookmark2002")) XCTAssertNil(db.isLocallyDeleted("bookmark2002")) XCTAssertEqual(false, db.isOverridden("bookmark3001")) XCTAssertNil(db.isLocallyDeleted("bookmark3001")) bookmarks.testFactory.removeByGUID("folderBBBBBB").succeeded() XCTAssertEqual(true, db.isOverridden("folderBBBBBB")) XCTAssertEqual(true, db.isLocallyDeleted("folderBBBBBB")) XCTAssertEqual(true, db.isOverridden("folderCCCCCC")) XCTAssertEqual(true, db.isLocallyDeleted("folderCCCCCC")) XCTAssertEqual(true, db.isOverridden("bookmark2002")) XCTAssertEqual(true, db.isLocallyDeleted("bookmark2002")) XCTAssertEqual(true, db.isOverridden("bookmark3001")) XCTAssertEqual(true, db.isLocallyDeleted("bookmark3001")) // Still there. XCTAssertNil(db.isOverridden(locallyAddedBookmark2)) XCTAssertFalse(db.isLocallyDeleted(locallyAddedBookmark2) ?? true) let menuChildrenAfterRecursiveDelete = getMenuChildren() XCTAssertEqual(1, menuChildrenAfterRecursiveDelete.count) XCTAssertEqual(locallyAddedBookmark2, menuChildrenAfterRecursiveDelete[0]) // Now let's delete by URL. XCTAssertEqual(false, db.isOverridden("bookmark1001")) XCTAssertNil(db.isLocallyDeleted("bookmark1001")) XCTAssertEqual(false, db.isOverridden("bookmark1002")) XCTAssertNil(db.isLocallyDeleted("bookmark1002")) bookmarks.testFactory.removeByURL("http://example.org/1").succeeded() // To conclude, check the entire hierarchy. // Menu: overridden, only the locally-added bookmark 2. // B, 3001, C, 2002: locally deleted. // A: overridden, children [separator101, 2001]. // No bookmarks with URL /1. XCTAssertEqual(true, db.isOverridden(BookmarkRoots.MenuFolderGUID)) XCTAssertEqual(true, db.isOverridden("folderBBBBBB")) XCTAssertEqual(true, db.isLocallyDeleted("folderBBBBBB")) XCTAssertEqual(true, db.isOverridden("folderCCCCCC")) XCTAssertEqual(true, db.isLocallyDeleted("folderCCCCCC")) XCTAssertEqual(true, db.isOverridden("bookmark2002")) XCTAssertEqual(true, db.isLocallyDeleted("bookmark2002")) XCTAssertEqual(true, db.isOverridden("bookmark3001")) XCTAssertEqual(true, db.isLocallyDeleted("bookmark3001")) XCTAssertEqual(true, db.isOverridden("bookmark1001")) XCTAssertEqual(true, db.isLocallyDeleted("bookmark1001")) XCTAssertEqual(true, db.isOverridden("bookmark1002")) XCTAssertEqual(true, db.isLocallyDeleted("bookmark1002")) let menuChildrenAfterURLDelete = getMenuChildren() XCTAssertEqual(1, menuChildrenAfterURLDelete.count) XCTAssertEqual(locallyAddedBookmark2, menuChildrenAfterURLDelete[0]) XCTAssertEqual(["separator101", "bookmark2001"], db.getChildrenOfFolder("folderAAAAAA")) } func testLocalAndMirror() { guard let db = getBrowserDB("TSQLBtestLocalAndMirror.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } // Preconditions. let rootGUIDs = [ BookmarkRoots.RootGUID, BookmarkRoots.MobileFolderGUID, BookmarkRoots.MenuFolderGUID, BookmarkRoots.ToolbarFolderGUID, BookmarkRoots.UnfiledFolderGUID, ] let positioned = [ BookmarkRoots.MenuFolderGUID, BookmarkRoots.ToolbarFolderGUID, BookmarkRoots.UnfiledFolderGUID, BookmarkRoots.MobileFolderGUID, ] XCTAssertEqual(rootGUIDs, db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) XCTAssertEqual(positioned, db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) ORDER BY idx")) XCTAssertEqual([], db.getGUIDs("SELECT guid FROM \(TableBookmarksMirror)")) XCTAssertEqual([], db.getGUIDs("SELECT child FROM \(TableBookmarksMirrorStructure)")) // Add a local bookmark. let bookmarks = SQLiteBookmarks(db: db) bookmarks.insertBookmark("http://example.org/".asURL!, title: "Example", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "The Mobile").succeeded() let rowA = db.getRecordByURL("http://example.org/", fromTable: TableBookmarksLocal) XCTAssertEqual(rowA.bookmarkURI, "http://example.org/") XCTAssertEqual(rowA.title, "Example") XCTAssertEqual(rowA.parentName, "The Mobile") XCTAssertEqual(rootGUIDs + [rowA.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) XCTAssertEqual([rowA.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(rowA.guid)) // Add another. Order should be maintained. bookmarks.insertBookmark("https://reddit.com/".asURL!, title: "Reddit", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "Mobile").succeeded() let rowB = db.getRecordByURL("https://reddit.com/", fromTable: TableBookmarksLocal) XCTAssertEqual(rowB.bookmarkURI, "https://reddit.com/") XCTAssertEqual(rowB.title, "Reddit") XCTAssertEqual(rootGUIDs + [rowA.guid, rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) XCTAssertEqual([rowA.guid, rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(rowA.guid)) XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(rowB.guid)) // The indices should be 0, 1. let positions = db.getPositionsForChildrenOfParent(BookmarkRoots.MobileFolderGUID, fromTable: TableBookmarksLocalStructure) XCTAssertEqual(positions.count, 2) XCTAssertEqual(positions[rowA.guid], 0) XCTAssertEqual(positions[rowB.guid], 1) // Delete the first. sync_status was New, so the row was immediately deleted. bookmarks.testFactory.removeByURL("http://example.org/").succeeded() XCTAssertEqual(rootGUIDs + [rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) XCTAssertEqual([rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) let positionsAfterDelete = db.getPositionsForChildrenOfParent(BookmarkRoots.MobileFolderGUID, fromTable: TableBookmarksLocalStructure) XCTAssertEqual(positionsAfterDelete.count, 1) XCTAssertEqual(positionsAfterDelete[rowB.guid], 0) // Manually shuffle all of these into the mirror, as if we were fully synchronized. db.moveLocalToMirrorForTesting() XCTAssertEqual([], db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id")) XCTAssertEqual([], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure)")) XCTAssertEqual(rootGUIDs + [rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksMirror) ORDER BY id")) XCTAssertEqual([rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksMirrorStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) let mirrorPositions = db.getPositionsForChildrenOfParent(BookmarkRoots.MobileFolderGUID, fromTable: TableBookmarksMirrorStructure) XCTAssertEqual(mirrorPositions.count, 1) XCTAssertEqual(mirrorPositions[rowB.guid], 0) // Now insert a new mobile bookmark. bookmarks.insertBookmark("https://letsencrypt.org/".asURL!, title: "Let's Encrypt", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "Mobile").succeeded() // The mobile bookmarks folder is overridden. XCTAssertEqual(true, db.isOverridden(BookmarkRoots.MobileFolderGUID)) // Our previous inserted bookmark is not. XCTAssertEqual(false, db.isOverridden(rowB.guid)) let rowC = db.getRecordByURL("https://letsencrypt.org/", fromTable: TableBookmarksLocal) // We have the old structure in the mirror. XCTAssertEqual(rootGUIDs + [rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksMirror) ORDER BY id")) XCTAssertEqual([rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksMirrorStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) // We have the new structure in the local table. XCTAssertEqual(Set([BookmarkRoots.MobileFolderGUID, rowC.guid]), Set(db.getGUIDs("SELECT guid FROM \(TableBookmarksLocal) ORDER BY id"))) XCTAssertEqual([rowB.guid, rowC.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) // Parent is changed. The new record is New. The unmodified and deleted records aren't present. XCTAssertNil(db.getSyncStatusForGUID(rowA.guid)) XCTAssertNil(db.getSyncStatusForGUID(rowB.guid)) XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(rowC.guid)) XCTAssertEqual(SyncStatus.changed, db.getSyncStatusForGUID(BookmarkRoots.MobileFolderGUID)) // If we delete the old record, we mark it as changed, and it's no longer in the structure. bookmarks.testFactory.removeByGUID(rowB.guid).succeeded() XCTAssertEqual(SyncStatus.changed, db.getSyncStatusForGUID(rowB.guid)) XCTAssertEqual([rowC.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) // Add a duplicate to test multi-deletion (unstar). bookmarks.insertBookmark("https://letsencrypt.org/".asURL!, title: "Let's Encrypt", favicon: nil, intoFolder: BookmarkRoots.MobileFolderGUID, withTitle: "Mobile").succeeded() let guidD = db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx").last! XCTAssertNotEqual(rowC.guid, guidD) XCTAssertEqual(SyncStatus.new, db.getSyncStatusForGUID(guidD)) XCTAssertEqual(SyncStatus.changed, db.getSyncStatusForGUID(BookmarkRoots.MobileFolderGUID)) // Delete by URL. // If we delete the new records, they just go away -- there's no server version to delete. bookmarks.testFactory.removeByURL(rowC.bookmarkURI!).succeeded() XCTAssertNil(db.getSyncStatusForGUID(rowC.guid)) XCTAssertNil(db.getSyncStatusForGUID(guidD)) XCTAssertEqual([], db.getGUIDs("SELECT child FROM \(TableBookmarksLocalStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) // The mirror structure is unchanged after all this. XCTAssertEqual(rootGUIDs + [rowB.guid], db.getGUIDs("SELECT guid FROM \(TableBookmarksMirror) ORDER BY id")) XCTAssertEqual([rowB.guid], db.getGUIDs("SELECT child FROM \(TableBookmarksMirrorStructure) WHERE parent = '\(BookmarkRoots.MobileFolderGUID)' ORDER BY idx")) } /* // This is dead test code after we eliminated the merged view. // Expect this to be ported to reflect post-sync state. func testBookmarkStructure() { guard let db = getBrowserDB("TSQLBtestBufferStorage.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = MergedSQLiteBookmarks(db: db) guard let model = bookmarks.modelForRoot().value.successValue else { XCTFail("Unable to get root.") return } let root = model.current // The root isn't useful. The merged bookmark implementation doesn't use it. XCTAssertEqual(root.guid, BookmarkRoots.RootGUID) XCTAssertEqual(0, root.count) // Specifically fetching the fake desktop folder works, though. guard let desktopModel = bookmarks.modelForFolder(BookmarkRoots.FakeDesktopFolderGUID).value.successValue else { XCTFail("Unable to get desktop bookmarks.") return } // It contains the toolbar. let desktopFolder = desktopModel.current XCTAssertEqual(desktopFolder.guid, BookmarkRoots.FakeDesktopFolderGUID) XCTAssertEqual(1, desktopFolder.count) guard let toolbarModel = desktopModel.selectFolder(BookmarkRoots.ToolbarFolderGUID).value.successValue else { XCTFail("Unable to get toolbar.") return } // The toolbar is the child, and it has the two bookmarks as entries. let toolbarFolder = toolbarModel.current XCTAssertEqual(toolbarFolder.guid, BookmarkRoots.ToolbarFolderGUID) XCTAssertEqual(2, toolbarFolder.count) guard let first = toolbarModel.current[0] else { XCTFail("Expected to get AAA.") return } guard let second = toolbarModel.current[1] else { XCTFail("Expected to get BBB.") return } XCTAssertEqual(first.guid, "aaaaaaaaaaaa") XCTAssertEqual(first.title, "AAA") XCTAssertEqual((first as? BookmarkItem)?.url, "http://getfirefox.com") XCTAssertEqual(second.guid, "bbbbbbbbbbbb") XCTAssertEqual(second.title, "BBB") XCTAssertEqual((second as? BookmarkItem)?.url, "http://getfirefox.com") let del: [BookmarkMirrorItem] = [BookmarkMirrorItem.deleted(BookmarkNodeType.Bookmark, guid: "aaaaaaaaaaaa", modified: Date.now())] bookmarks.applyRecords(del, withMaxVars: 1) guard let newToolbar = bookmarks.modelForFolder(BookmarkRoots.ToolbarFolderGUID).value.successValue else { XCTFail("Unable to get toolbar.") return } XCTAssertEqual(newToolbar.current.count, 1) XCTAssertEqual(newToolbar.current[0]?.guid, "bbbbbbbbbbbb") } */ func testBufferStorage() { guard let db = getBrowserDB("TSQLBtestBufferStorage.db", files: self.files) else { XCTFail("Unable to create browser DB.") return } let bookmarks = SQLiteBookmarkBufferStorage(db: db) let record1 = BookmarkMirrorItem.bookmark("aaaaaaaaaaaa", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "AAA", description: "AAA desc", URI: "http://getfirefox.com", tags: "[]", keyword: nil) let record2 = BookmarkMirrorItem.bookmark("bbbbbbbbbbbb", dateAdded: Date.now(), modified: Date.now() + 10, hasDupe: false, parentID: BookmarkRoots.ToolbarFolderGUID, parentName: "Bookmarks Toolbar", title: "BBB", description: "BBB desc", URI: "http://getfirefox.com", tags: "[]", keyword: nil) let toolbar = BookmarkMirrorItem.folder("toolbar", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: "places", parentName: "", title: "Bookmarks Toolbar", description: "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar", children: ["aaaaaaaaaaaa", "bbbbbbbbbbbb"]) let recordsA: [BookmarkMirrorItem] = [record1, toolbar, record2] bookmarks.applyRecords(recordsA, withMaxVars: 3).succeeded() // Insert mobile bookmarks as produced by Firefox 40-something on desktop. // swiftlint:disable line_length let children = ["M87np9Vfh_2s", "-JxRyqNte-ue", "6lIQzUtbjE8O", "eOg3jPSslzXl", "1WJIi9EjQErp", "z5uRo45Rvfbd", "EK3lcNd0sUFN", "gFD3GTljgu12", "eRZGsbN1ew9-", "widfEdgGn9de", "l7eTOR4Uf6xq", "vPbxG-gpN4Rb", "4dwJ8CototFe", "zK-kw9Ii6ScW", "eDmDU-gtEFW6", "lKjqWQaL_syt", "ETVDvWgGT31Q", "3Z_bMIHPSZQ8", "Fqu4_bJOk7fT", "Uo_5K1QrA67j", "gDTXNg4m1AJZ", "zpds8P-9xews", "87zjNtVGPtEp", "ZJru8Sn3qhW7", "txVnzBBBOgLP", "JTnRqFaj_oNa", "soaMlfmM4kjR", "g8AcVBjo6IRf", "uPUDaiG4q637", "rfq2bUud_w4d", "XBGxsiuUG2UD", "-VQRnJlyAvMs", "6wu7TScKdTU7", "ZeFji2hLVpLj", "HpCn_TVizMWX", "IPR5HZwRdlwi", "00JFOGuWnhWB", "P1jb3qKt32Vg", "D6MQJ43V1Ir5", "qWSoXFteRfsq", "o2avfYqEdomL", "xRS0U0YnjK9G", "VgOgzE_xfP4w", "SwP3rMJGvoO3", "Hf2jEgI_-PWa", "AyhmBi7Cv598", "-PaMuzTJXxVk", "JMhYrg8SlY5K", "SQeySEjzyplL", "GTAwd2UkEQEe", "x3RsZj5Ilebr", "sRZWZqPi74FP", "amHR50TpygA6", "XSk782ceVNN6", "ipiMyYQzeypI", "ph2k3Nqfhau4", "m5JKC3hAEQ0H", "yTVerkmQbNxk", "7taA6FbbbUbH", "PZvpbSRuJLPs", "C8atoa25U94F", "KOfNJk_ISLc6", "Bt74lBG9tJq6", "BuHoY2rUhuKA", "XTmoWKnwfIPl", "ZATwa3oTD1m0", "e8TczN5It6Am", "6kCUYs8hQtKg", "jDD8s5aiKoex", "QmpmcrYwLU29", "nCRcekynuJ08", "resttaI4J9tu", "EKSX3HV55VU3", "2-yCz0EIsVls", "sSeeGw3VbBY-", "qfpCrU34w9y0", "RKDgzPWecD6m", "5SgXEKu_dICW", "R143WAeB5E5r", "8Ns4-NiKG62r", "4AHuZDvop5XX", "YCP1OsO1goFF", "CYYaU1mQ_N6t", "UGkzEOMK8cuU", "1RzZOarkzQBa", "qSW2Z3cZSI9c", "ooPlKEAfQsnn", "jIUScoKLiXQt", "bjNTKugzRRL1", "hR24ZVnHUZcs", "3j2IDAZgUyYi", "xnWcy-sQDJRu", "UCcgJqGk3bTV", "WSSRWeptH9tq", "4ugv47OGD2E2", "XboCZgUx-x3x", "HrmWqiqsuLrm", "OjdxvRJ3Jb6j"] // swiftlint:enable line_length let mA = BookmarkMirrorItem.bookmark("jIUScoKLiXQt", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: "mobile", parentName: "mobile", title: "Join the Engineering Leisure Class — Medium", description: nil, URI: "https://medium.com/@chrisloer/join-the-engineering-leisure-class-b3083c09a78e", tags: "[]", keyword: nil) let mB = BookmarkMirrorItem.folder("UjAHxFOGEqU8", dateAdded: Date.now(), modified: Date.now(), hasDupe: false, parentID: "places", parentName: "", title: "mobile", description: nil, children: children) bookmarks.applyRecords([mA, mB]).succeeded() func childCount(_ parent: GUID) -> Int? { let sql = "SELECT COUNT(*) AS childCount FROM \(TableBookmarksBufferStructure) WHERE parent = ?" let args: Args = [parent] return db.runQuery(sql, args: args, factory: { $0["childCount"] as! Int }).value.successValue?[0] } // We have children. XCTAssertEqual(children.count, childCount("UjAHxFOGEqU8")) // Insert an empty mobile bookmarks folder, so we can verify that the structure table is wiped. let mBEmpty = BookmarkMirrorItem.folder("UjAHxFOGEqU8", dateAdded: Date.now(), modified: Date.now() + 1, hasDupe: false, parentID: "places", parentName: "", title: "mobile", description: nil, children: []) bookmarks.applyRecords([mBEmpty]).succeeded() // We no longer have children. XCTAssertEqual(0, childCount("UjAHxFOGEqU8")) } }