This repository has been archived by the owner. It is now read-only.
Permalink
Cannot retrieve contributors at this time
executable file
1150 lines (969 sloc)
57 KB
| /* 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<BookmarkTree>) { | |
| 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<BookmarkTree>) { | |
| 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<GUID>(["iwasdeleted"])) | |
| XCTAssertEqual(partialBuffer.orphans, Set<GUID>(["changedbookmark"])) | |
| XCTAssertEqual(partialBuffer.subtreeGUIDs, Set<GUID>(["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")) | |
| } | |
| } |