diff options
Diffstat (limited to 'toolkit/components/places/tests/sync/head_sync.js')
-rw-r--r-- | toolkit/components/places/tests/sync/head_sync.js | 482 |
1 files changed, 482 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/sync/head_sync.js b/toolkit/components/places/tests/sync/head_sync.js new file mode 100644 index 0000000000..549ee3d0b4 --- /dev/null +++ b/toolkit/components/places/tests/sync/head_sync.js @@ -0,0 +1,482 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Import common head. +{ + /* import-globals-from ../head_common.js */ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. + +var { CanonicalJSON } = ChromeUtils.import( + "resource://gre/modules/CanonicalJSON.jsm" +); +var { Log } = ChromeUtils.importESModule("resource://gre/modules/Log.sys.mjs"); +var { ObjectUtils } = ChromeUtils.import( + "resource://gre/modules/ObjectUtils.jsm" +); +var { PlacesSyncUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesSyncUtils.sys.mjs" +); +var { SyncedBookmarksMirror } = ChromeUtils.importESModule( + "resource://gre/modules/SyncedBookmarksMirror.sys.mjs" +); +var { CommonUtils } = ChromeUtils.import("resource://services-common/utils.js"); +var { FileTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/FileTestUtils.sys.mjs" +); +var { + HTTP_400, + HTTP_401, + HTTP_402, + HTTP_403, + HTTP_404, + HTTP_405, + HTTP_406, + HTTP_407, + HTTP_408, + HTTP_409, + HTTP_410, + HTTP_411, + HTTP_412, + HTTP_413, + HTTP_414, + HTTP_415, + HTTP_417, + HTTP_500, + HTTP_501, + HTTP_502, + HTTP_503, + HTTP_504, + HTTP_505, + HttpError, + HttpServer, +} = ChromeUtils.import("resource://testing-common/httpd.js"); + +// These titles are defined in Database::CreateBookmarkRoots +const BookmarksMenuTitle = "menu"; +const BookmarksToolbarTitle = "toolbar"; +const UnfiledBookmarksTitle = "unfiled"; +const MobileBookmarksTitle = "mobile"; + +function run_test() { + let bufLog = Log.repository.getLogger("Sync.Engine.Bookmarks.Mirror"); + bufLog.level = Log.Level.All; + + let sqliteLog = Log.repository.getLogger("Sqlite"); + sqliteLog.level = Log.Level.Error; + + let formatter = new Log.BasicFormatter(); + let appender = new Log.DumpAppender(formatter); + appender.level = Log.Level.All; + + for (let log of [bufLog, sqliteLog]) { + log.addAppender(appender); + } + + do_get_profile(); + run_next_test(); +} + +// A test helper to insert local roots directly into Places, since the public +// bookmarks APIs no longer support custom roots. +async function insertLocalRoot({ guid, title }) { + await PlacesUtils.withConnectionWrapper("insertLocalRoot", async function( + db + ) { + let dateAdded = PlacesUtils.toPRTime(new Date()); + await db.execute( + ` + INSERT INTO moz_bookmarks(guid, type, parent, position, title, + dateAdded, lastModified) + VALUES(:guid, :type, (SELECT id FROM moz_bookmarks + WHERE guid = :parentGuid), + (SELECT COUNT(*) FROM moz_bookmarks + WHERE parent = (SELECT id FROM moz_bookmarks + WHERE guid = :parentGuid)), + :title, :dateAdded, :dateAdded)`, + { + guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.rootGuid, + title, + dateAdded, + } + ); + }); +} + +// Returns a `CryptoWrapper`-like object that wraps the Sync record cleartext. +// This exists to avoid importing `record.js` from Sync. +function makeRecord(cleartext) { + return new Proxy( + { cleartext }, + { + get(target, property, receiver) { + if (property == "cleartext") { + return target.cleartext; + } + if (property == "cleartextToString") { + return () => JSON.stringify(target.cleartext); + } + return target.cleartext[property]; + }, + set(target, property, value, receiver) { + if (property == "cleartext") { + target.cleartext = value; + } else if (property != "cleartextToString") { + target.cleartext[property] = value; + } + }, + has(target, property) { + return property == "cleartext" || property in target.cleartext; + }, + deleteProperty(target, property) {}, + ownKeys(target) { + return ["cleartext", ...Reflect.ownKeys(target)]; + }, + } + ); +} + +async function storeRecords(buf, records, options) { + await buf.store(records.map(makeRecord), options); +} + +async function storeChangesInMirror(buf, changesToUpload) { + let cleartexts = []; + for (let recordId in changesToUpload) { + changesToUpload[recordId].synced = true; + cleartexts.push(changesToUpload[recordId].cleartext); + } + await storeRecords(buf, cleartexts, { needsMerge: false }); + await PlacesSyncUtils.bookmarks.pushChanges(changesToUpload); +} + +function inspectChangeRecords(changeRecords) { + let results = { updated: [], deleted: [] }; + for (let [id, record] of Object.entries(changeRecords)) { + (record.tombstone ? results.deleted : results.updated).push(id); + } + results.updated.sort(); + results.deleted.sort(); + return results; +} + +async function promiseManyDatesAdded(guids) { + let datesAdded = new Map(); + let db = await PlacesUtils.promiseDBConnection(); + for (let chunk of PlacesUtils.chunkArray(guids, 100)) { + let rows = await db.executeCached( + ` + SELECT guid, dateAdded FROM moz_bookmarks + WHERE guid IN (${new Array(chunk.length).fill("?").join(",")})`, + chunk + ); + if (rows.length != chunk.length) { + throw new TypeError("Can't fetch date added for nonexistent items"); + } + for (let row of rows) { + let dateAdded = row.getResultByName("dateAdded") / 1000; + datesAdded.set(row.getResultByName("guid"), dateAdded); + } + } + return datesAdded; +} + +async function fetchLocalTree(rootGuid) { + function bookmarkNodeToInfo(node) { + let { guid, index, title, typeCode: type } = node; + let itemInfo = { guid, index, title, type }; + if (node.annos) { + let syncableAnnos = node.annos.filter(anno => + [PlacesUtils.LMANNO_FEEDURI, PlacesUtils.LMANNO_SITEURI].includes( + anno.name + ) + ); + if (syncableAnnos.length) { + itemInfo.annos = syncableAnnos; + } + } + if (node.uri) { + itemInfo.url = node.uri; + } + if (node.keyword) { + itemInfo.keyword = node.keyword; + } + if (node.children) { + itemInfo.children = node.children.map(bookmarkNodeToInfo); + } + if (node.tags) { + itemInfo.tags = node.tags.split(",").sort(); + } + return itemInfo; + } + let root = await PlacesUtils.promiseBookmarksTree(rootGuid); + return bookmarkNodeToInfo(root); +} + +async function assertLocalTree(rootGuid, expected, message) { + let actual = await fetchLocalTree(rootGuid); + if (!ObjectUtils.deepEqual(actual, expected)) { + info( + `Expected structure for ${rootGuid}: ${CanonicalJSON.stringify(expected)}` + ); + info( + `Actual structure for ${rootGuid}: ${CanonicalJSON.stringify(actual)}` + ); + throw new Assert.constructor.AssertionError({ actual, expected, message }); + } +} + +function makeLivemarkServer() { + let server = new HttpServer(); + server.registerPrefixHandler("/feed/", do_get_file("./livemark.xml")); + server.start(-1); + return { + server, + get site() { + let { identity } = server; + let host = identity.primaryHost.includes(":") + ? `[${identity.primaryHost}]` + : identity.primaryHost; + return `${identity.primaryScheme}://${host}:${identity.primaryPort}`; + }, + stopServer() { + return new Promise(resolve => server.stop(resolve)); + }, + }; +} + +function shuffle(array) { + let results = []; + for (let i = 0; i < array.length; ++i) { + let randomIndex = Math.floor(Math.random() * (i + 1)); + results[i] = results[randomIndex]; + results[randomIndex] = array[i]; + } + return results; +} + +async function fetchAllKeywords(info) { + let entries = []; + await PlacesUtils.keywords.fetch(info, entry => entries.push(entry)); + return entries; +} + +async function openMirror(name, options = {}) { + let buf = await SyncedBookmarksMirror.open({ + path: `${name}_buf.sqlite`, + recordStepTelemetry(...args) { + if (options.recordStepTelemetry) { + options.recordStepTelemetry.call(this, ...args); + } + }, + recordValidationTelemetry(...args) { + if (options.recordValidationTelemetry) { + options.recordValidationTelemetry.call(this, ...args); + } + }, + }); + return buf; +} + +function BookmarkObserver({ ignoreDates = true, skipTags = false } = {}) { + this.notifications = []; + this.ignoreDates = ignoreDates; + this.skipTags = skipTags; + this.handlePlacesEvents = this.handlePlacesEvents.bind(this); +} + +BookmarkObserver.prototype = { + handlePlacesEvents(events) { + for (let event of events) { + switch (event.type) { + case "bookmark-added": { + if (this.skipTags && event.isTagging) { + continue; + } + let params = { + itemId: event.id, + parentId: event.parentId, + index: event.index, + type: event.itemType, + urlHref: event.url, + title: event.title, + guid: event.guid, + parentGuid: event.parentGuid, + source: event.source, + }; + if (!this.ignoreDates) { + params.dateAdded = event.dateAdded; + } + this.notifications.push({ name: "bookmark-added", params }); + break; + } + case "bookmark-removed": { + if (this.skipTags && event.isTagging) { + continue; + } + // Since we are now skipping tags on the listener side we don't + // prevent unTagging notifications from going out. These events cause empty + // tags folders to be removed which creates another bookmark-removed notification + if ( + this.skipTags && + event.parentGuid == PlacesUtils.bookmarks.tagsGuid + ) { + continue; + } + let params = { + itemId: event.id, + parentId: event.parentId, + index: event.index, + type: event.itemType, + urlHref: event.url || null, + title: event.title, + guid: event.guid, + parentGuid: event.parentGuid, + source: event.source, + }; + this.notifications.push({ name: "bookmark-removed", params }); + break; + } + case "bookmark-moved": { + const params = { + itemId: event.id, + type: event.itemType, + urlHref: event.url, + source: event.source, + guid: event.guid, + newIndex: event.index, + newParentGuid: event.parentGuid, + oldIndex: event.oldIndex, + oldParentGuid: event.oldParentGuid, + isTagging: event.isTagging, + }; + this.notifications.push({ name: "bookmark-moved", params }); + break; + } + case "bookmark-guid-changed": { + const params = { + itemId: event.id, + type: event.itemType, + urlHref: event.url, + guid: event.guid, + parentGuid: event.parentGuid, + source: event.source, + isTagging: event.isTagging, + }; + this.notifications.push({ name: "bookmark-guid-changed", params }); + break; + } + case "bookmark-title-changed": { + const params = { + itemId: event.id, + guid: event.guid, + title: event.title, + parentGuid: event.parentGuid, + }; + this.notifications.push({ name: "bookmark-title-changed", params }); + break; + } + case "bookmark-url-changed": { + const params = { + itemId: event.id, + type: event.itemType, + urlHref: event.url, + guid: event.guid, + parentGuid: event.parentGuid, + source: event.source, + isTagging: event.isTagging, + }; + this.notifications.push({ name: "bookmark-url-changed", params }); + break; + } + } + } + }, + onItemChanged( + itemId, + property, + isAnnoProperty, + newValue, + lastModified, + type, + parentId, + guid, + parentGuid, + oldValue, + source + ) { + let params = { + itemId, + property, + isAnnoProperty, + newValue, + type, + parentId, + guid, + parentGuid, + oldValue, + source, + }; + if (!this.ignoreDates) { + params.lastModified = lastModified; + } + this.notifications.push({ name: "onItemChanged", params }); + }, + + QueryInterface: ChromeUtils.generateQI(["nsINavBookmarkObserver"]), + + check(expectedNotifications) { + PlacesUtils.bookmarks.removeObserver(this); + PlacesUtils.observers.removeListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-guid-changed", + "bookmark-title-changed", + "bookmark-url-changed", + ], + this.handlePlacesEvents + ); + if (!ObjectUtils.deepEqual(this.notifications, expectedNotifications)) { + info(`Expected notifications: ${JSON.stringify(expectedNotifications)}`); + info(`Actual notifications: ${JSON.stringify(this.notifications)}`); + throw new Assert.constructor.AssertionError({ + actual: this.notifications, + expected: expectedNotifications, + }); + } + }, +}; + +function expectBookmarkChangeNotifications(options) { + let observer = new BookmarkObserver(options); + PlacesUtils.bookmarks.addObserver(observer); + PlacesUtils.observers.addListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-guid-changed", + "bookmark-title-changed", + "bookmark-url-changed", + ], + observer.handlePlacesEvents + ); + return observer; +} + +// Copies a support file to a temporary fixture file, allowing the support +// file to be reused for multiple tests. +async function setupFixtureFile(fixturePath) { + let fixtureFile = do_get_file(fixturePath); + let tempFile = FileTestUtils.getTempFile(fixturePath); + await IOUtils.copy(fixtureFile.path, tempFile.path); + return tempFile; +} |