summaryrefslogtreecommitdiffstats
path: root/services/sync/tests/unit/test_bookmark_engine.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/tests/unit/test_bookmark_engine.js')
-rw-r--r--services/sync/tests/unit/test_bookmark_engine.js1417
1 files changed, 1417 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_bookmark_engine.js b/services/sync/tests/unit/test_bookmark_engine.js
new file mode 100644
index 0000000000..a45ad3a643
--- /dev/null
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -0,0 +1,1417 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { BookmarkHTMLUtils } = ChromeUtils.import(
+ "resource://gre/modules/BookmarkHTMLUtils.jsm"
+);
+const { BookmarkJSONUtils } = ChromeUtils.import(
+ "resource://gre/modules/BookmarkJSONUtils.jsm"
+);
+const { SyncedBookmarksMirror } = ChromeUtils.import(
+ "resource://gre/modules/SyncedBookmarksMirror.jsm"
+);
+const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const {
+ Bookmark,
+ BookmarkFolder,
+ BookmarksEngine,
+ Livemark,
+} = ChromeUtils.import("resource://services-sync/engines/bookmarks.js");
+const { Service } = ChromeUtils.import("resource://services-sync/service.js");
+
+var recordedEvents = [];
+
+function checkRecordedEvents(object, expected, message) {
+ // Ignore event telemetry from the merger.
+ let checkEvents = recordedEvents.filter(event => event.object == object);
+ deepEqual(checkEvents, expected, message);
+ // and clear the list so future checks are easier to write.
+ recordedEvents = [];
+}
+
+async function fetchAllRecordIds() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(`
+ WITH RECURSIVE
+ syncedItems(id, guid) AS (
+ SELECT b.id, b.guid FROM moz_bookmarks b
+ WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
+ 'mobile______')
+ UNION ALL
+ SELECT b.id, b.guid FROM moz_bookmarks b
+ JOIN syncedItems s ON b.parent = s.id
+ )
+ SELECT guid FROM syncedItems`);
+ let recordIds = new Set();
+ for (let row of rows) {
+ let recordId = PlacesSyncUtils.bookmarks.guidToRecordId(
+ row.getResultByName("guid")
+ );
+ recordIds.add(recordId);
+ }
+ return recordIds;
+}
+
+async function cleanupEngine(engine) {
+ await engine.resetClient();
+ await engine._store.wipe();
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ // Note we don't finalize the engine here as add_bookmark_test() does.
+}
+
+async function cleanup(engine, server) {
+ await promiseStopServer(server);
+ await cleanupEngine(engine);
+}
+
+add_task(async function setup() {
+ await generateNewKeys(Service.collectionKeys);
+ await Service.engineManager.unregister("bookmarks");
+
+ Service.recordTelemetryEvent = (object, method, value, extra = undefined) => {
+ recordedEvents.push({ object, method, value, extra });
+ };
+});
+
+add_task(async function test_buffer_timeout() {
+ await Service.recordManager.clearCache();
+ await PlacesSyncUtils.bookmarks.reset();
+ let engine = new BookmarksEngine(Service);
+ engine._newWatchdog = function() {
+ // Return an already-aborted watchdog, so that we can abort merges
+ // immediately.
+ let watchdog = Async.watchdog();
+ watchdog.controller.abort();
+ return watchdog;
+ };
+ await engine.initialize();
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+ let collection = server.user("foo").collection("bookmarks");
+
+ try {
+ info("Insert local bookmarks");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ url: "http://example.com/a",
+ title: "A",
+ },
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ ],
+ });
+
+ info("Insert remote bookmarks");
+ collection.insert(
+ "menu",
+ encryptPayload({
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ title: "menu",
+ children: ["bookmarkCCCC", "bookmarkDDDD"],
+ })
+ );
+ collection.insert(
+ "bookmarkCCCC",
+ encryptPayload({
+ id: "bookmarkCCCC",
+ type: "bookmark",
+ parentid: "menu",
+ bmkUri: "http://example.com/c",
+ title: "C",
+ })
+ );
+ collection.insert(
+ "bookmarkDDDD",
+ encryptPayload({
+ id: "bookmarkDDDD",
+ type: "bookmark",
+ parentid: "menu",
+ bmkUri: "http://example.com/d",
+ title: "D",
+ })
+ );
+
+ info("We expect this sync to fail");
+ await Assert.rejects(
+ sync_engine_and_validate_telem(engine, true),
+ ex => ex.name == "InterruptedError"
+ );
+ } finally {
+ await cleanup(engine, server);
+ await engine.finalize();
+ }
+});
+
+add_bookmark_test(async function test_maintenance_after_failure(engine) {
+ _("Ensure we try to run maintenance if the engine fails to sync");
+
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ try {
+ let syncStartup = engine._syncStartup;
+ let syncError = new Error("Something is rotten in the state of Places");
+ engine._syncStartup = function() {
+ throw syncError;
+ };
+
+ Services.prefs.clearUserPref("places.database.lastMaintenance");
+
+ _("Ensure the sync fails and we run maintenance");
+ await Assert.rejects(
+ sync_engine_and_validate_telem(engine, true),
+ ex => ex == syncError
+ );
+ checkRecordedEvents(
+ "maintenance",
+ [
+ {
+ object: "maintenance",
+ method: "run",
+ value: "bookmarks",
+ extra: undefined,
+ },
+ ],
+ "Should record event for first maintenance run"
+ );
+
+ _("Sync again, but ensure maintenance doesn't run");
+ await Assert.rejects(
+ sync_engine_and_validate_telem(engine, true),
+ ex => ex == syncError
+ );
+ checkRecordedEvents(
+ "maintenance",
+ [],
+ "Should not record event if maintenance didn't run"
+ );
+
+ _("Fast-forward last maintenance pref; ensure maintenance runs");
+ Services.prefs.setIntPref(
+ "places.database.lastMaintenance",
+ Date.now() / 1000 - 14400
+ );
+ await Assert.rejects(
+ sync_engine_and_validate_telem(engine, true),
+ ex => ex == syncError
+ );
+ checkRecordedEvents(
+ "maintenance",
+ [
+ {
+ object: "maintenance",
+ method: "run",
+ value: "bookmarks",
+ extra: undefined,
+ },
+ ],
+ "Should record event for second maintenance run"
+ );
+
+ _("Fix sync failure; ensure we report success after maintenance");
+ engine._syncStartup = syncStartup;
+ await sync_engine_and_validate_telem(engine, false);
+ checkRecordedEvents(
+ "maintenance",
+ [
+ {
+ object: "maintenance",
+ method: "fix",
+ value: "bookmarks",
+ extra: undefined,
+ },
+ ],
+ "Should record event for successful sync after second maintenance"
+ );
+
+ await sync_engine_and_validate_telem(engine, false);
+ checkRecordedEvents(
+ "maintenance",
+ [],
+ "Should not record maintenance events after successful sync"
+ );
+ } finally {
+ await cleanup(engine, server);
+ }
+});
+
+add_bookmark_test(async function test_delete_invalid_roots_from_server(engine) {
+ _("Ensure that we delete the Places and Reading List roots from the server.");
+
+ enableValidationPrefs();
+
+ let store = engine._store;
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ let collection = server.user("foo").collection("bookmarks");
+
+ engine._tracker.start();
+
+ try {
+ let placesRecord = await store.createRecord("places");
+ collection.insert("places", encryptPayload(placesRecord.cleartext));
+
+ let listBmk = new Bookmark("bookmarks", Utils.makeGUID());
+ listBmk.bmkUri = "https://example.com";
+ listBmk.title = "Example reading list entry";
+ listBmk.parentName = "Reading List";
+ listBmk.parentid = "readinglist";
+ collection.insert(listBmk.id, encryptPayload(listBmk.cleartext));
+
+ let readingList = new BookmarkFolder("bookmarks", "readinglist");
+ readingList.title = "Reading List";
+ readingList.children = [listBmk.id];
+ readingList.parentName = "";
+ readingList.parentid = "places";
+ collection.insert("readinglist", encryptPayload(readingList.cleartext));
+
+ // Note that we don't insert a record for the toolbar, so the engine will
+ // report a parent-child disagreement, since Firefox's `parentid` is
+ // `toolbar`.
+ let newBmk = new Bookmark("bookmarks", Utils.makeGUID());
+ newBmk.bmkUri = "http://getfirefox.com";
+ newBmk.title = "Get Firefox!";
+ newBmk.parentName = "Bookmarks Toolbar";
+ newBmk.parentid = "toolbar";
+ collection.insert(newBmk.id, encryptPayload(newBmk.cleartext));
+
+ deepEqual(
+ collection.keys().sort(),
+ ["places", "readinglist", listBmk.id, newBmk.id].sort(),
+ "Should store Places root, reading list items, and new bookmark on server"
+ );
+
+ let ping = await sync_engine_and_validate_telem(engine, true);
+ // In a real sync, the engine is named `bookmarks-buffered`.
+ // However, `sync_engine_and_validate_telem` simulates a sync where
+ // the engine isn't registered with the engine manager, so the recorder
+ // doesn't see its `overrideTelemetryName`.
+ let engineData = ping.engines.find(e => e.name == "bookmarks");
+ ok(engineData.validation, "Bookmarks engine should always run validation");
+ equal(
+ engineData.validation.checked,
+ 6,
+ "Bookmarks engine should validate all items"
+ );
+ deepEqual(
+ engineData.validation.problems,
+ [
+ {
+ name: "parentChildDisagreements",
+ count: 1,
+ },
+ ],
+ "Bookmarks engine should report parent-child disagreement"
+ );
+ deepEqual(
+ engineData.steps.map(step => step.name),
+ [
+ "fetchLocalTree",
+ "fetchRemoteTree",
+ "merge",
+ "apply",
+ "notifyObservers",
+ "fetchLocalChangeRecords",
+ ],
+ "Bookmarks engine should report all merge steps"
+ );
+
+ await Assert.rejects(
+ PlacesUtils.promiseItemId("readinglist"),
+ /no item found for the given GUID/,
+ "Should not apply Reading List root"
+ );
+ await Assert.rejects(
+ PlacesUtils.promiseItemId(listBmk.id),
+ /no item found for the given GUID/,
+ "Should not apply items in Reading List"
+ );
+ ok(
+ (await PlacesUtils.promiseItemId(newBmk.id)) > 0,
+ "Should apply new bookmark"
+ );
+
+ deepEqual(
+ collection.keys().sort(),
+ ["menu", "mobile", "toolbar", "unfiled", newBmk.id].sort(),
+ "Should remove Places root and reading list items from server; upload local roots"
+ );
+ } finally {
+ await cleanup(engine, server);
+ }
+});
+
+add_bookmark_test(async function test_processIncoming_error_orderChildren(
+ engine
+) {
+ _(
+ "Ensure that _orderChildren() is called even when _processIncoming() throws an error."
+ );
+
+ let store = engine._store;
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ let collection = server.user("foo").collection("bookmarks");
+
+ try {
+ let folder1 = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "Folder 1",
+ });
+
+ let bmk1 = await PlacesUtils.bookmarks.insert({
+ parentGuid: folder1.guid,
+ url: "http://getfirefox.com/",
+ title: "Get Firefox!",
+ });
+ let bmk2 = await PlacesUtils.bookmarks.insert({
+ parentGuid: folder1.guid,
+ url: "http://getthunderbird.com/",
+ title: "Get Thunderbird!",
+ });
+
+ let toolbar_record = await store.createRecord("toolbar");
+ collection.insert("toolbar", encryptPayload(toolbar_record.cleartext));
+
+ let bmk1_record = await store.createRecord(bmk1.guid);
+ collection.insert(bmk1.guid, encryptPayload(bmk1_record.cleartext));
+
+ let bmk2_record = await store.createRecord(bmk2.guid);
+ collection.insert(bmk2.guid, encryptPayload(bmk2_record.cleartext));
+
+ // Create a server record for folder1 where we flip the order of
+ // the children.
+ let folder1_record = await store.createRecord(folder1.guid);
+ let folder1_payload = folder1_record.cleartext;
+ folder1_payload.children.reverse();
+ collection.insert(folder1.guid, encryptPayload(folder1_payload));
+
+ // Create a bogus record that when synced down will provoke a
+ // network error which in turn provokes an exception in _processIncoming.
+ const BOGUS_GUID = "zzzzzzzzzzzz";
+ let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
+ bogus_record.get = function get() {
+ throw new Error("Sync this!");
+ };
+
+ // Make the 10 minutes old so it will only be synced in the toFetch phase.
+ bogus_record.modified = new_timestamp() - 60 * 10;
+ await engine.setLastSync(new_timestamp() - 60);
+ engine.toFetch = new SerializableSet([BOGUS_GUID]);
+
+ let error;
+ try {
+ await sync_engine_and_validate_telem(engine, true);
+ } catch (ex) {
+ error = ex;
+ }
+ ok(!!error);
+
+ // Verify that the bookmark order has been applied.
+ folder1_record = await store.createRecord(folder1.guid);
+ let new_children = folder1_record.children;
+ Assert.deepEqual(
+ new_children.sort(),
+ [folder1_payload.children[0], folder1_payload.children[1]].sort()
+ );
+
+ let localChildIds = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
+ folder1.guid
+ );
+ Assert.deepEqual(localChildIds.sort(), [bmk2.guid, bmk1.guid].sort());
+ } finally {
+ await cleanup(engine, server);
+ }
+});
+
+add_bookmark_test(async function test_restorePromptsReupload(engine) {
+ await test_restoreOrImport(engine, { replace: true });
+});
+
+add_bookmark_test(async function test_importPromptsReupload(engine) {
+ await test_restoreOrImport(engine, { replace: false });
+});
+
+// Test a JSON restore or HTML import. Use JSON if `replace` is `true`, or
+// HTML otherwise.
+async function test_restoreOrImport(engine, { replace }) {
+ let verb = replace ? "restore" : "import";
+ let verbing = replace ? "restoring" : "importing";
+ let bookmarkUtils = replace ? BookmarkJSONUtils : BookmarkHTMLUtils;
+
+ _(`Ensure that ${verbing} from a backup will reupload all records.`);
+
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ let collection = server.user("foo").collection("bookmarks");
+
+ engine._tracker.start(); // We skip usual startup...
+
+ try {
+ let folder1 = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "Folder 1",
+ });
+
+ _("Create a single record.");
+ let bmk1 = await PlacesUtils.bookmarks.insert({
+ parentGuid: folder1.guid,
+ url: "http://getfirefox.com/",
+ title: "Get Firefox!",
+ });
+ _(`Get Firefox!: ${bmk1.guid}`);
+
+ let backupFilePath = OS.Path.join(
+ OS.Constants.Path.tmpDir,
+ `t_b_e_${Date.now()}.json`
+ );
+
+ _("Make a backup.");
+
+ await bookmarkUtils.exportToFile(backupFilePath);
+
+ _("Create a different record and sync.");
+ let bmk2 = await PlacesUtils.bookmarks.insert({
+ parentGuid: folder1.guid,
+ url: "http://getthunderbird.com/",
+ title: "Get Thunderbird!",
+ });
+ _(`Get Thunderbird!: ${bmk2.guid}`);
+
+ await PlacesUtils.bookmarks.remove(bmk1.guid);
+
+ let error;
+ try {
+ await sync_engine_and_validate_telem(engine, false);
+ } catch (ex) {
+ error = ex;
+ _("Got error: " + Log.exceptionStr(ex));
+ }
+ Assert.ok(!error);
+
+ _(
+ "Verify that there's only one bookmark on the server, and it's Thunderbird."
+ );
+ // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu...
+ let wbos = collection.keys(function(id) {
+ return !["menu", "toolbar", "mobile", "unfiled", folder1.guid].includes(
+ id
+ );
+ });
+ Assert.equal(wbos.length, 1);
+ Assert.equal(wbos[0], bmk2.guid);
+
+ _(`Now ${verb} from a backup.`);
+ await bookmarkUtils.importFromFile(backupFilePath, { replace });
+
+ // If `replace` is `true`, we'll wipe the server on the next sync.
+ let bookmarksCollection = server.user("foo").collection("bookmarks");
+ _("Verify that we didn't wipe the server.");
+ Assert.ok(!!bookmarksCollection);
+
+ _("Ensure we have the bookmarks we expect locally.");
+ let recordIds = await fetchAllRecordIds();
+ _("GUIDs: " + JSON.stringify([...recordIds]));
+
+ let bookmarkRecordIds = new Map();
+ let count = 0;
+ for (let recordId of recordIds) {
+ count++;
+ let info = await PlacesUtils.bookmarks.fetch(
+ PlacesSyncUtils.bookmarks.recordIdToGuid(recordId)
+ );
+ // Only one bookmark, so _all_ should be Firefox!
+ if (info.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
+ _(`Found URI ${info.url.href} for record ID ${recordId}`);
+ bookmarkRecordIds.set(info.url.href, recordId);
+ }
+ }
+ Assert.ok(bookmarkRecordIds.has("http://getfirefox.com/"));
+ if (!replace) {
+ Assert.ok(bookmarkRecordIds.has("http://getthunderbird.com/"));
+ }
+
+ _("Have the correct number of IDs locally, too.");
+ let expectedResults = [
+ "menu",
+ "toolbar",
+ "mobile",
+ "unfiled",
+ folder1.guid,
+ bmk1.guid,
+ ];
+ if (!replace) {
+ expectedResults.push("toolbar", folder1.guid, bmk2.guid);
+ }
+ Assert.equal(count, expectedResults.length);
+
+ _("Sync again. This'll wipe bookmarks from the server.");
+ try {
+ await sync_engine_and_validate_telem(engine, false);
+ } catch (ex) {
+ error = ex;
+ _("Got error: " + Log.exceptionStr(ex));
+ }
+ Assert.ok(!error);
+
+ _("Verify that there's the right bookmarks on the server.");
+ // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu...
+ let payloads = server
+ .user("foo")
+ .collection("bookmarks")
+ .payloads();
+ let bookmarkWBOs = payloads.filter(function(wbo) {
+ return wbo.type == "bookmark";
+ });
+
+ let folderWBOs = payloads.filter(function(wbo) {
+ return (
+ wbo.type == "folder" &&
+ wbo.id != "menu" &&
+ wbo.id != "toolbar" &&
+ wbo.id != "unfiled" &&
+ wbo.id != "mobile" &&
+ wbo.parentid != "menu"
+ );
+ });
+
+ let expectedFX = {
+ id: bookmarkRecordIds.get("http://getfirefox.com/"),
+ bmkUri: "http://getfirefox.com/",
+ title: "Get Firefox!",
+ };
+ let expectedTB = {
+ id: bookmarkRecordIds.get("http://getthunderbird.com/"),
+ bmkUri: "http://getthunderbird.com/",
+ title: "Get Thunderbird!",
+ };
+
+ let expectedBookmarks;
+ if (replace) {
+ expectedBookmarks = [expectedFX];
+ } else {
+ expectedBookmarks = [expectedTB, expectedFX];
+ }
+
+ doCheckWBOs(bookmarkWBOs, expectedBookmarks);
+
+ _("Our old friend Folder 1 is still in play.");
+ let expectedFolder1 = { title: "Folder 1" };
+
+ let expectedFolders;
+ if (replace) {
+ expectedFolders = [expectedFolder1];
+ } else {
+ expectedFolders = [expectedFolder1, expectedFolder1];
+ }
+
+ doCheckWBOs(folderWBOs, expectedFolders);
+ } finally {
+ await cleanup(engine, server);
+ }
+}
+
+function doCheckWBOs(WBOs, expected) {
+ Assert.equal(WBOs.length, expected.length);
+ for (let i = 0; i < expected.length; i++) {
+ let lhs = WBOs[i];
+ let rhs = expected[i];
+ if ("id" in rhs) {
+ Assert.equal(lhs.id, rhs.id);
+ }
+ if ("bmkUri" in rhs) {
+ Assert.equal(lhs.bmkUri, rhs.bmkUri);
+ }
+ if ("title" in rhs) {
+ Assert.equal(lhs.title, rhs.title);
+ }
+ }
+}
+
+function FakeRecord(constructor, r) {
+ this.defaultCleartext = constructor.prototype.defaultCleartext;
+ constructor.call(this, "bookmarks", r.id);
+ for (let x in r) {
+ this[x] = r[x];
+ }
+ // Borrow the constructor's conversion functions.
+ this.toSyncBookmark = constructor.prototype.toSyncBookmark;
+ this.cleartextToString = constructor.prototype.cleartextToString;
+}
+
+// Bug 632287.
+// (Note that `test_mismatched_folder_types()` in
+// toolkit/components/places/tests/sync/test_bookmark_kinds.js is an exact
+// copy of this test, so it's fine to remove it as part of bug 1449730)
+add_task(async function test_mismatched_types() {
+ _(
+ "Ensure that handling a record that changes type causes deletion " +
+ "then re-adding."
+ );
+
+ let oldRecord = {
+ id: "l1nZZXfB8nC7",
+ type: "folder",
+ parentName: "Bookmarks Toolbar",
+ title: "Innerst i Sneglehode",
+ description: null,
+ parentid: "toolbar",
+ };
+
+ let newRecord = {
+ id: "l1nZZXfB8nC7",
+ type: "livemark",
+ siteUri: "http://sneglehode.wordpress.com/",
+ feedUri: "http://sneglehode.wordpress.com/feed/",
+ parentName: "Bookmarks Toolbar",
+ title: "Innerst i Sneglehode",
+ description: null,
+ children: [
+ "HCRq40Rnxhrd",
+ "YeyWCV1RVsYw",
+ "GCceVZMhvMbP",
+ "sYi2hevdArlF",
+ "vjbZlPlSyGY8",
+ "UtjUhVyrpeG6",
+ "rVq8WMG2wfZI",
+ "Lx0tcy43ZKhZ",
+ "oT74WwV8_j4P",
+ "IztsItWVSo3-",
+ ],
+ parentid: "toolbar",
+ };
+
+ let engine = new BookmarksEngine(Service);
+ await engine.initialize();
+ let store = engine._store;
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ try {
+ let oldR = new FakeRecord(BookmarkFolder, oldRecord);
+ let newR = new FakeRecord(Livemark, newRecord);
+ oldR.parentid = PlacesUtils.bookmarks.toolbarGuid;
+ newR.parentid = PlacesUtils.bookmarks.toolbarGuid;
+
+ await store.applyIncoming(oldR);
+ await engine._apply();
+ _("Applied old. It's a folder.");
+ let oldID = await PlacesUtils.promiseItemId(oldR.id);
+ _("Old ID: " + oldID);
+ let oldInfo = await PlacesUtils.bookmarks.fetch(oldR.id);
+ Assert.equal(oldInfo.type, PlacesUtils.bookmarks.TYPE_FOLDER);
+
+ await store.applyIncoming(newR);
+ await engine._apply();
+ await Assert.rejects(
+ PlacesUtils.promiseItemId(newR.id),
+ /no item found for the given GUID/,
+ "Should not apply Livemark"
+ );
+ } finally {
+ await cleanup(engine, server);
+ await engine.finalize();
+ }
+});
+
+add_bookmark_test(async function test_misreconciled_root(engine) {
+ _("Ensure that we don't reconcile an arbitrary record with a root.");
+
+ let store = engine._store;
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ // Log real hard for this test.
+ store._log.trace = store._log.debug;
+ engine._log.trace = engine._log.debug;
+
+ await engine._syncStartup();
+
+ // Let's find out where the toolbar is right now.
+ let toolbarBefore = await store.createRecord("toolbar", "bookmarks");
+ let toolbarIDBefore = await PlacesUtils.promiseItemId(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ Assert.notEqual(-1, toolbarIDBefore);
+
+ let parentRecordIDBefore = toolbarBefore.parentid;
+ let parentGUIDBefore = PlacesSyncUtils.bookmarks.recordIdToGuid(
+ parentRecordIDBefore
+ );
+ let parentIDBefore = await PlacesUtils.promiseItemId(parentGUIDBefore);
+ Assert.equal("string", typeof parentGUIDBefore);
+
+ _("Current parent: " + parentGUIDBefore + " (" + parentIDBefore + ").");
+
+ let to_apply = {
+ id: "zzzzzzzzzzzz",
+ type: "folder",
+ title: "Bookmarks Toolbar",
+ description: "Now you're for it.",
+ parentName: "",
+ parentid: "mobile", // Why not?
+ children: [],
+ };
+
+ let rec = new FakeRecord(BookmarkFolder, to_apply);
+
+ _("Applying record.");
+ store.applyIncomingBatch([rec]);
+
+ // Ensure that afterwards, toolbar is still there.
+ // As of 2012-12-05, this only passes because Places doesn't use "toolbar" as
+ // the real GUID, instead using a generated one. Sync does the translation.
+ let toolbarAfter = await store.createRecord("toolbar", "bookmarks");
+ let parentRecordIDAfter = toolbarAfter.parentid;
+ let parentGUIDAfter = PlacesSyncUtils.bookmarks.recordIdToGuid(
+ parentRecordIDAfter
+ );
+ let parentIDAfter = await PlacesUtils.promiseItemId(parentGUIDAfter);
+ Assert.equal(
+ await PlacesUtils.promiseItemGuid(toolbarIDBefore),
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ Assert.equal(parentGUIDBefore, parentGUIDAfter);
+ Assert.equal(parentIDBefore, parentIDAfter);
+
+ await cleanup(engine, server);
+});
+
+add_bookmark_test(async function test_sync_dateAdded(engine) {
+ await Service.recordManager.clearCache();
+ await PlacesSyncUtils.bookmarks.reset();
+ let store = engine._store;
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ let collection = server.user("foo").collection("bookmarks");
+
+ // TODO: Avoid random orange (bug 1374599), this is only necessary
+ // intermittently - reset the last sync date so that we'll get all bookmarks.
+ await engine.setLastSync(1);
+
+ engine._tracker.start(); // We skip usual startup...
+
+ // Just matters that it's in the past, not how far.
+ let now = Date.now();
+ let oneYearMS = 365 * 24 * 60 * 60 * 1000;
+
+ try {
+ let toolbar = new BookmarkFolder("bookmarks", "toolbar");
+ toolbar.title = "toolbar";
+ toolbar.parentName = "";
+ toolbar.parentid = "places";
+ toolbar.children = [
+ "abcdefabcdef",
+ "aaaaaaaaaaaa",
+ "bbbbbbbbbbbb",
+ "cccccccccccc",
+ "dddddddddddd",
+ "eeeeeeeeeeee",
+ ];
+ collection.insert("toolbar", encryptPayload(toolbar.cleartext));
+
+ let item1GUID = "abcdefabcdef";
+ let item1 = new Bookmark("bookmarks", item1GUID);
+ item1.bmkUri = "https://example.com";
+ item1.title = "asdf";
+ item1.parentName = "Bookmarks Toolbar";
+ item1.parentid = "toolbar";
+ item1.dateAdded = now - oneYearMS;
+ collection.insert(item1GUID, encryptPayload(item1.cleartext));
+
+ let item2GUID = "aaaaaaaaaaaa";
+ let item2 = new Bookmark("bookmarks", item2GUID);
+ item2.bmkUri = "https://example.com/2";
+ item2.title = "asdf2";
+ item2.parentName = "Bookmarks Toolbar";
+ item2.parentid = "toolbar";
+ item2.dateAdded = now + oneYearMS;
+ const item2LastModified = now / 1000 - 100;
+ collection.insert(
+ item2GUID,
+ encryptPayload(item2.cleartext),
+ item2LastModified
+ );
+
+ let item3GUID = "bbbbbbbbbbbb";
+ let item3 = new Bookmark("bookmarks", item3GUID);
+ item3.bmkUri = "https://example.com/3";
+ item3.title = "asdf3";
+ item3.parentName = "Bookmarks Toolbar";
+ item3.parentid = "toolbar";
+ // no dateAdded
+ collection.insert(item3GUID, encryptPayload(item3.cleartext));
+
+ let item4GUID = "cccccccccccc";
+ let item4 = new Bookmark("bookmarks", item4GUID);
+ item4.bmkUri = "https://example.com/4";
+ item4.title = "asdf4";
+ item4.parentName = "Bookmarks Toolbar";
+ item4.parentid = "toolbar";
+ // no dateAdded, but lastModified in past
+ const item4LastModified = (now - oneYearMS) / 1000;
+ collection.insert(
+ item4GUID,
+ encryptPayload(item4.cleartext),
+ item4LastModified
+ );
+
+ let item5GUID = "dddddddddddd";
+ let item5 = new Bookmark("bookmarks", item5GUID);
+ item5.bmkUri = "https://example.com/5";
+ item5.title = "asdf5";
+ item5.parentName = "Bookmarks Toolbar";
+ item5.parentid = "toolbar";
+ // no dateAdded, lastModified in (near) future.
+ const item5LastModified = (now + 60000) / 1000;
+ collection.insert(
+ item5GUID,
+ encryptPayload(item5.cleartext),
+ item5LastModified
+ );
+
+ let item6GUID = "eeeeeeeeeeee";
+ let item6 = new Bookmark("bookmarks", item6GUID);
+ item6.bmkUri = "https://example.com/6";
+ item6.title = "asdf6";
+ item6.parentName = "Bookmarks Toolbar";
+ item6.parentid = "toolbar";
+ const item6LastModified = (now - oneYearMS) / 1000;
+ collection.insert(
+ item6GUID,
+ encryptPayload(item6.cleartext),
+ item6LastModified
+ );
+
+ await sync_engine_and_validate_telem(engine, false);
+
+ let record1 = await store.createRecord(item1GUID);
+ let record2 = await store.createRecord(item2GUID);
+
+ equal(
+ item1.dateAdded,
+ record1.dateAdded,
+ "dateAdded in past should be synced"
+ );
+ equal(
+ record2.dateAdded,
+ item2LastModified * 1000,
+ "dateAdded in future should be ignored in favor of last modified"
+ );
+
+ let record3 = await store.createRecord(item3GUID);
+
+ ok(record3.dateAdded);
+ // Make sure it's within 24 hours of the right timestamp... This is a little
+ // dodgey but we only really care that it's basically accurate and has the
+ // right day.
+ ok(Math.abs(Date.now() - record3.dateAdded) < 24 * 60 * 60 * 1000);
+
+ let record4 = await store.createRecord(item4GUID);
+ equal(
+ record4.dateAdded,
+ item4LastModified * 1000,
+ "If no dateAdded is provided, lastModified should be used"
+ );
+
+ let record5 = await store.createRecord(item5GUID);
+ equal(
+ record5.dateAdded,
+ item5LastModified * 1000,
+ "If no dateAdded is provided, lastModified should be used (even if it's in the future)"
+ );
+
+ // Update item2 and try resyncing it.
+ item2.dateAdded = now - 100000;
+ collection.insert(
+ item2GUID,
+ encryptPayload(item2.cleartext),
+ now / 1000 - 50
+ );
+
+ // Also, add a local bookmark and make sure its date added makes it up to the server
+ let bz = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "https://bugzilla.mozilla.org/",
+ title: "Bugzilla",
+ });
+
+ // last sync did a POST, which doesn't advance its lastModified value.
+ // Next sync of the engine doesn't hit info/collections, so lastModified
+ // remains stale. Setting it to null side-steps that.
+ engine.lastModified = null;
+ await sync_engine_and_validate_telem(engine, false);
+
+ let newRecord2 = await store.createRecord(item2GUID);
+ equal(
+ newRecord2.dateAdded,
+ item2.dateAdded,
+ "dateAdded update should work for earlier date"
+ );
+
+ let bzWBO = collection.cleartext(bz.guid);
+ ok(bzWBO.dateAdded, "Locally added dateAdded lost");
+
+ let localRecord = await store.createRecord(bz.guid);
+ equal(
+ bzWBO.dateAdded,
+ localRecord.dateAdded,
+ "dateAdded should not change during upload"
+ );
+
+ item2.dateAdded += 10000;
+ collection.insert(
+ item2GUID,
+ encryptPayload(item2.cleartext),
+ now / 1000 - 10
+ );
+
+ engine.lastModified = null;
+ await sync_engine_and_validate_telem(engine, false);
+
+ let newerRecord2 = await store.createRecord(item2GUID);
+ equal(
+ newerRecord2.dateAdded,
+ newRecord2.dateAdded,
+ "dateAdded update should be ignored for later date if we know an earlier one "
+ );
+ } finally {
+ await cleanup(engine, server);
+ }
+});
+
+add_task(async function test_buffer_hasDupe() {
+ await Service.recordManager.clearCache();
+ await PlacesSyncUtils.bookmarks.reset();
+ let engine = new BookmarksEngine(Service);
+ await engine.initialize();
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+ let collection = server.user("foo").collection("bookmarks");
+ engine._tracker.start(); // We skip usual startup...
+ try {
+ let guid1 = Utils.makeGUID();
+ let guid2 = Utils.makeGUID();
+ await PlacesUtils.bookmarks.insert({
+ guid: guid1,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "https://www.example.com",
+ title: "example.com",
+ });
+ await PlacesUtils.bookmarks.insert({
+ guid: guid2,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "https://www.example.com",
+ title: "example.com",
+ });
+
+ await sync_engine_and_validate_telem(engine, false);
+ // Make sure we set hasDupe on outgoing records
+ Assert.ok(collection.payloads().every(payload => payload.hasDupe));
+
+ await PlacesUtils.bookmarks.remove(guid1);
+
+ // Make sure it works for weakly uploaded records
+ engine.addForWeakUpload(guid2);
+
+ await sync_engine_and_validate_telem(engine, false);
+
+ let tombstone = JSON.parse(
+ JSON.parse(collection.payload(guid1)).ciphertext
+ );
+ // We shouldn't set hasDupe on tombstones.
+ Assert.ok(tombstone.deleted);
+ Assert.ok(!tombstone.hasDupe);
+
+ let record = JSON.parse(JSON.parse(collection.payload(guid2)).ciphertext);
+ // We should set hasDupe on weakly uploaded records.
+ Assert.ok(!record.deleted);
+ Assert.ok(
+ record.hasDupe,
+ "Bookmarks bookmark engine should set hasDupe for weakly uploaded records."
+ );
+
+ await sync_engine_and_validate_telem(engine, false);
+ } finally {
+ await cleanup(engine, server);
+ await engine.finalize();
+ }
+});
+
+// Bug 890217.
+add_bookmark_test(async function test_sync_imap_URLs(engine) {
+ await Service.recordManager.clearCache();
+ await PlacesSyncUtils.bookmarks.reset();
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ let collection = server.user("foo").collection("bookmarks");
+
+ engine._tracker.start(); // We skip usual startup...
+
+ try {
+ collection.insert(
+ "menu",
+ encryptPayload({
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ title: "Bookmarks Menu",
+ children: ["bookmarkAAAA"],
+ })
+ );
+ collection.insert(
+ "bookmarkAAAA",
+ encryptPayload({
+ id: "bookmarkAAAA",
+ type: "bookmark",
+ parentid: "menu",
+ bmkUri:
+ "imap://vs@eleven.vs.solnicky.cz:993/fetch%3EUID%3E/" +
+ "INBOX%3E56291?part=1.2&type=image/jpeg&filename=" +
+ "invalidazPrahy.jpg",
+ title:
+ "invalidazPrahy.jpg (JPEG Image, 1280x1024 pixels) - Scaled (71%)",
+ })
+ );
+
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkBBBB",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url:
+ "imap://eleven.vs.solnicky.cz:993/fetch%3EUID%3E/" +
+ "CURRENT%3E2433?part=1.2&type=text/html&filename=TomEdwards.html",
+ title: "TomEdwards.html",
+ });
+
+ await sync_engine_and_validate_telem(engine, false);
+
+ let aInfo = await PlacesUtils.bookmarks.fetch("bookmarkAAAA");
+ equal(
+ aInfo.url.href,
+ "imap://vs@eleven.vs.solnicky.cz:993/" +
+ "fetch%3EUID%3E/INBOX%3E56291?part=1.2&type=image/jpeg&filename=" +
+ "invalidazPrahy.jpg",
+ "Remote bookmark A with IMAP URL should exist locally"
+ );
+
+ let bPayload = collection.cleartext("bookmarkBBBB");
+ equal(
+ bPayload.bmkUri,
+ "imap://eleven.vs.solnicky.cz:993/" +
+ "fetch%3EUID%3E/CURRENT%3E2433?part=1.2&type=text/html&filename=" +
+ "TomEdwards.html",
+ "Local bookmark B with IMAP URL should exist remotely"
+ );
+ } finally {
+ await cleanup(engine, server);
+ }
+});
+
+add_task(async function test_resume_buffer() {
+ await Service.recordManager.clearCache();
+ let engine = new BookmarksEngine(Service);
+ await engine.initialize();
+ await engine._store.wipe();
+ await engine.resetClient();
+
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ let collection = server.user("foo").collection("bookmarks");
+
+ engine._tracker.start(); // We skip usual startup...
+
+ const batchChunkSize = 50;
+
+ engine._store._batchChunkSize = batchChunkSize;
+ try {
+ let children = [];
+
+ let timestamp = round_timestamp(Date.now());
+ // Add two chunks worth of records to the server
+ for (let i = 0; i < batchChunkSize * 2; ++i) {
+ let cleartext = {
+ id: Utils.makeGUID(),
+ type: "bookmark",
+ parentid: "toolbar",
+ title: `Bookmark ${i}`,
+ parentName: "Bookmarks Toolbar",
+ bmkUri: `https://example.com/${i}`,
+ };
+ let wbo = collection.insert(
+ cleartext.id,
+ encryptPayload(cleartext),
+ timestamp + 10 * i
+ );
+ // Something that is effectively random, but deterministic.
+ // (This is just to ensure we don't accidentally start using the
+ // sortindex again).
+ wbo.sortindex = 1000 + Math.round(Math.sin(i / 5) * 100);
+ children.push(cleartext.id);
+ }
+
+ // Add the parent of those records, and ensure its timestamp is the most recent.
+ collection.insert(
+ "toolbar",
+ encryptPayload({
+ id: "toolbar",
+ type: "folder",
+ parentid: "places",
+ title: "Bookmarks Toolbar",
+ children,
+ }),
+ timestamp + 10 * children.length
+ );
+
+ // Replace applyIncomingBatch with a custom one that calls the original,
+ // but forces it to throw on the 2nd chunk.
+ let origApplyIncomingBatch = engine._store.applyIncomingBatch;
+ engine._store.applyIncomingBatch = function(records) {
+ if (records.length > batchChunkSize) {
+ // Hacky way to make reading from the batchChunkSize'th record throw.
+ delete records[batchChunkSize];
+ Object.defineProperty(records, batchChunkSize, {
+ get() {
+ throw new Error("D:");
+ },
+ });
+ }
+ return origApplyIncomingBatch.call(this, records);
+ };
+
+ let caughtError;
+ _("We expect this to fail");
+ try {
+ await sync_engine_and_validate_telem(engine, true);
+ } catch (e) {
+ caughtError = e;
+ }
+ Assert.ok(caughtError, "Expected engine.sync to throw");
+ Assert.equal(caughtError.message, "D:");
+
+ // The buffer subtracts one second from the actual timestamp.
+ let lastSync = (await engine.getLastSync()) + 1;
+ // We poisoned the batchChunkSize'th record, so the last successfully
+ // applied record will be batchChunkSize - 1.
+ let expectedLastSync = timestamp + 10 * (batchChunkSize - 1);
+ Assert.equal(expectedLastSync, lastSync);
+
+ engine._store.applyIncomingBatch = origApplyIncomingBatch;
+
+ await sync_engine_and_validate_telem(engine, false);
+
+ // Check that all the children made it onto the correct record.
+ let toolbarRecord = await engine._store.createRecord("toolbar");
+ Assert.deepEqual(toolbarRecord.children.sort(), children.sort());
+ } finally {
+ await cleanup(engine, server);
+ await engine.finalize();
+ }
+});
+
+add_bookmark_test(async function test_livemarks(engine) {
+ _("Ensure we replace new and existing livemarks with tombstones");
+
+ let server = await serverForFoo(engine);
+ await SyncTestingInfrastructure(server);
+
+ let collection = server.user("foo").collection("bookmarks");
+ let now = Date.now();
+
+ try {
+ _("Insert existing livemark");
+ let modifiedForA = now - 5 * 60 * 1000;
+ await PlacesUtils.bookmarks.insert({
+ guid: "livemarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "A",
+ lastModified: new Date(modifiedForA),
+ dateAdded: new Date(modifiedForA),
+ source: PlacesUtils.bookmarks.SOURCE_SYNC,
+ });
+ collection.insert(
+ "menu",
+ encryptPayload({
+ id: "menu",
+ type: "folder",
+ parentName: "",
+ title: "menu",
+ children: ["livemarkAAAA"],
+ parentid: "places",
+ }),
+ round_timestamp(modifiedForA)
+ );
+ collection.insert(
+ "livemarkAAAA",
+ encryptPayload({
+ id: "livemarkAAAA",
+ type: "livemark",
+ feedUri: "http://example.com/a",
+ parentName: "menu",
+ title: "A",
+ parentid: "menu",
+ }),
+ round_timestamp(modifiedForA)
+ );
+
+ _("Insert remotely updated livemark");
+ await PlacesUtils.bookmarks.insert({
+ guid: "livemarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "B",
+ lastModified: new Date(now),
+ dateAdded: new Date(now),
+ });
+ collection.insert(
+ "toolbar",
+ encryptPayload({
+ id: "toolbar",
+ type: "folder",
+ parentName: "",
+ title: "toolbar",
+ children: ["livemarkBBBB"],
+ parentid: "places",
+ }),
+ round_timestamp(now)
+ );
+ collection.insert(
+ "livemarkBBBB",
+ encryptPayload({
+ id: "livemarkBBBB",
+ type: "livemark",
+ feedUri: "http://example.com/b",
+ parentName: "toolbar",
+ title: "B",
+ parentid: "toolbar",
+ }),
+ round_timestamp(now)
+ );
+
+ _("Insert new remote livemark");
+ collection.insert(
+ "unfiled",
+ encryptPayload({
+ id: "unfiled",
+ type: "folder",
+ parentName: "",
+ title: "unfiled",
+ children: ["livemarkCCCC"],
+ parentid: "places",
+ }),
+ round_timestamp(now)
+ );
+ collection.insert(
+ "livemarkCCCC",
+ encryptPayload({
+ id: "livemarkCCCC",
+ type: "livemark",
+ feedUri: "http://example.com/c",
+ parentName: "unfiled",
+ title: "C",
+ parentid: "unfiled",
+ }),
+ round_timestamp(now)
+ );
+
+ _("Bump last sync time to ignore A");
+ await engine.setLastSync(round_timestamp(now) - 60);
+
+ _("Sync");
+ await sync_engine_and_validate_telem(engine, false);
+
+ deepEqual(
+ collection.keys().sort(),
+ [
+ "livemarkAAAA",
+ "livemarkBBBB",
+ "livemarkCCCC",
+ "menu",
+ "mobile",
+ "toolbar",
+ "unfiled",
+ ],
+ "Should store original livemark A and tombstones for B and C on server"
+ );
+
+ let payloads = collection.payloads();
+
+ deepEqual(
+ payloads.find(payload => payload.id == "menu").children,
+ ["livemarkAAAA"],
+ "Should keep A in menu"
+ );
+ ok(
+ !payloads.find(payload => payload.id == "livemarkAAAA").deleted,
+ "Should not upload tombstone for A"
+ );
+
+ deepEqual(
+ payloads.find(payload => payload.id == "toolbar").children,
+ [],
+ "Should remove B from toolbar"
+ );
+ ok(
+ payloads.find(payload => payload.id == "livemarkBBBB").deleted,
+ "Should upload tombstone for B"
+ );
+
+ deepEqual(
+ payloads.find(payload => payload.id == "unfiled").children,
+ [],
+ "Should remove C from unfiled"
+ );
+ ok(
+ payloads.find(payload => payload.id == "livemarkCCCC").deleted,
+ "Should replace C with tombstone"
+ );
+
+ await assertBookmarksTreeMatches(
+ "",
+ [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ {
+ guid: "livemarkAAAA",
+ index: 0,
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ },
+ ],
+ "Should keep A and remove B locally"
+ );
+ } finally {
+ await cleanup(engine, server);
+ }
+});