summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/sync/test_sync_utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/sync/test_sync_utils.js')
-rw-r--r--toolkit/components/places/tests/sync/test_sync_utils.js3081
1 files changed, 3081 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/sync/test_sync_utils.js b/toolkit/components/places/tests/sync/test_sync_utils.js
new file mode 100644
index 0000000000..c0e57677dd
--- /dev/null
+++ b/toolkit/components/places/tests/sync/test_sync_utils.js
@@ -0,0 +1,3081 @@
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+var makeGuid = PlacesUtils.history.makeGuid;
+
+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 assertTagForURLs(tag, urls, message) {
+ let taggedURLs = new Set();
+ await PlacesUtils.bookmarks.fetch({ tags: [tag] }, b =>
+ taggedURLs.add(b.url.href)
+ );
+ deepEqual(
+ Array.from(taggedURLs).sort(compareAscending),
+ urls.sort(compareAscending),
+ message
+ );
+}
+
+function assertURLHasTags(url, tags, message) {
+ let actualTags = PlacesUtils.tagging.getTagsForURI(uri(url));
+ deepEqual(actualTags.sort(compareAscending), tags, message);
+}
+
+var populateTree = async function populate(parentGuid, ...items) {
+ let guids = {};
+
+ for (let index = 0; index < items.length; index++) {
+ let item = items[index];
+ let guid = makeGuid();
+
+ switch (item.kind) {
+ case "bookmark":
+ case "query":
+ await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: item.url,
+ title: item.title,
+ parentGuid,
+ guid,
+ index,
+ });
+ break;
+
+ case "separator":
+ await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid,
+ guid,
+ });
+ break;
+
+ case "folder":
+ await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: item.title,
+ parentGuid,
+ guid,
+ });
+ if (item.children) {
+ Object.assign(guids, await populate(guid, ...item.children));
+ }
+ break;
+
+ default:
+ throw new Error(`Unsupported item type: ${item.type}`);
+ }
+
+ guids[item.title] = guid;
+ }
+
+ return guids;
+};
+
+var moveSyncedBookmarksToUnsyncedParent = async function() {
+ info("Insert synced bookmarks");
+ let syncedGuids = await populateTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ kind: "folder",
+ title: "folder",
+ children: [
+ {
+ kind: "bookmark",
+ title: "childBmk",
+ url: "https://example.org",
+ },
+ ],
+ },
+ {
+ kind: "bookmark",
+ title: "topBmk",
+ url: "https://example.com",
+ }
+ );
+ // Pretend we've synced each bookmark at least once.
+ await PlacesTestUtils.setBookmarkSyncFields(
+ ...Object.values(syncedGuids).map(guid => ({
+ guid,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ }))
+ );
+
+ info("Make new folder");
+ let unsyncedFolder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "unsyncedFolder",
+ });
+
+ info("Move synced bookmarks into unsynced new folder");
+ for (let guid of Object.values(syncedGuids)) {
+ await PlacesUtils.bookmarks.update({
+ guid,
+ parentGuid: unsyncedFolder.guid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+ }
+
+ return { syncedGuids, unsyncedFolder };
+};
+
+var setChangesSynced = async function(changes) {
+ for (let recordId in changes) {
+ changes[recordId].synced = true;
+ }
+ await PlacesSyncUtils.bookmarks.pushChanges(changes);
+};
+
+var ignoreChangedRoots = async function() {
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ let expectedRoots = ["menu", "mobile", "toolbar", "unfiled"];
+ if (!ObjectUtils.deepEqual(Object.keys(changes).sort(), expectedRoots)) {
+ // Make sure the previous test cleaned up.
+ throw new Error(
+ `Unexpected changes at start of test: ${JSON.stringify(changes)}`
+ );
+ }
+ await setChangesSynced(changes);
+};
+
+add_task(async function test_fetchURLFrecency() {
+ // Add visits to the following URLs and then check if frecency for those URLs is not -1.
+ let arrayOfURLsToVisit = [
+ "https://www.mozilla.org/en-US/",
+ "http://getfirefox.com",
+ "http://getthunderbird.com",
+ ];
+ for (let url of arrayOfURLsToVisit) {
+ await PlacesTestUtils.addVisits(url);
+ }
+ for (let url of arrayOfURLsToVisit) {
+ let frecency = await PlacesSyncUtils.history.fetchURLFrecency(url);
+ equal(typeof frecency, "number", "The frecency should be of type: number");
+ notEqual(
+ frecency,
+ -1,
+ "The frecency of this url should be different than -1"
+ );
+ }
+ // Do not add visits to the following URLs, and then check if frecency for those URLs is -1.
+ let arrayOfURLsNotVisited = ["https://bugzilla.org", "https://example.org"];
+ for (let url of arrayOfURLsNotVisited) {
+ let frecency = await PlacesSyncUtils.history.fetchURLFrecency(url);
+ equal(frecency, -1, "The frecency of this url should be -1");
+ }
+
+ // Remove the visits added during this test.
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_determineNonSyncableGuids() {
+ // Add visits to the following URLs with different transition types.
+ let arrayOfVisits = [
+ { uri: "https://www.mozilla.org/en-US/", transition: TRANSITION_TYPED },
+ { uri: "http://getfirefox.com/", transition: TRANSITION_LINK },
+ { uri: "http://getthunderbird.com/", transition: TRANSITION_FRAMED_LINK },
+ { uri: "http://downloads.com/", transition: TRANSITION_DOWNLOAD },
+ ];
+ for (let visit of arrayOfVisits) {
+ await PlacesTestUtils.addVisits(visit);
+ }
+
+ // Fetch the guid for each visit.
+ let guids = [];
+ let dictURLGuid = {};
+ for (let visit of arrayOfVisits) {
+ let guid = await PlacesSyncUtils.history.fetchGuidForURL(visit.uri);
+ guids.push(guid);
+ dictURLGuid[visit.uri] = guid;
+ }
+
+ // Filter the visits.
+ let filteredGuids = await PlacesSyncUtils.history.determineNonSyncableGuids(
+ guids
+ );
+
+ let filtered = [TRANSITION_FRAMED_LINK, TRANSITION_DOWNLOAD];
+ // Check if the filtered visits are of type TRANSITION_FRAMED_LINK.
+ for (let visit of arrayOfVisits) {
+ if (filtered.includes(visit.transition)) {
+ ok(
+ filteredGuids.includes(dictURLGuid[visit.uri]),
+ "This url should be one of the filtered guids."
+ );
+ } else {
+ ok(
+ !filteredGuids.includes(dictURLGuid[visit.uri]),
+ "This url should not be one of the filtered guids."
+ );
+ }
+ }
+
+ // Remove the visits added during this test.
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_changeGuid() {
+ // Add some visits of the following URLs.
+ let arrayOfURLsToVisit = [
+ "https://www.mozilla.org/en-US/",
+ "http://getfirefox.com/",
+ "http://getthunderbird.com/",
+ ];
+ for (let url of arrayOfURLsToVisit) {
+ await PlacesTestUtils.addVisits(url);
+ }
+
+ for (let url of arrayOfURLsToVisit) {
+ let originalGuid = await PlacesSyncUtils.history.fetchGuidForURL(url);
+ let newGuid = makeGuid();
+
+ // Change the original GUID for the new GUID.
+ await PlacesSyncUtils.history.changeGuid(url, newGuid);
+
+ // Fetch the GUID for this URL.
+ let newGuidFetched = await PlacesSyncUtils.history.fetchGuidForURL(url);
+
+ // Check that the URL has the new GUID as its GUID and not the original one.
+ equal(
+ newGuid,
+ newGuidFetched,
+ "These should be equal since we changed the guid for the visit."
+ );
+ notEqual(
+ originalGuid,
+ newGuidFetched,
+ "These should be different since we changed the guid for the visit."
+ );
+ }
+
+ // Remove the visits added during this test.
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_fetchVisitsForURL() {
+ // Get the date for this moment and a date for a minute ago.
+ let now = new Date();
+ let aMinuteAgo = new Date(now.getTime() - 1 * 60000);
+
+ // Add some visits of the following URLs, specifying the transition and the visit date.
+ let arrayOfVisits = [
+ {
+ uri: "https://www.mozilla.org/en-US/",
+ transition: TRANSITION_TYPED,
+ visitDate: aMinuteAgo,
+ },
+ {
+ uri: "http://getfirefox.com/",
+ transition: TRANSITION_LINK,
+ visitDate: aMinuteAgo,
+ },
+ {
+ uri: "http://getthunderbird.com/",
+ transition: TRANSITION_LINK,
+ visitDate: aMinuteAgo,
+ },
+ ];
+ for (let elem of arrayOfVisits) {
+ await PlacesTestUtils.addVisits(elem);
+ }
+
+ for (let elem of arrayOfVisits) {
+ // Fetch all the visits for this URL.
+ let visits = await PlacesSyncUtils.history.fetchVisitsForURL(elem.uri);
+ // Since the visit we added will be the last one in the collection of visits, we get the index of it.
+ let iLast = visits.length - 1;
+
+ // The date is saved in _micro_seconds, here we change it to milliseconds.
+ let dateInMilliseconds = visits[iLast].date * 0.001;
+
+ // Check that the info we provided for this URL is the same one retrieved.
+ equal(
+ dateInMilliseconds,
+ elem.visitDate.getTime(),
+ "The date we provided should be the same we retrieved."
+ );
+ equal(
+ visits[iLast].type,
+ elem.transition,
+ "The transition type we provided should be the same we retrieved."
+ );
+ }
+
+ // Remove the visits added during this test.
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_fetchGuidForURL() {
+ // Add some visits of the following URLs.
+ let arrayOfURLsToVisit = [
+ "https://www.mozilla.org/en-US/",
+ "http://getfirefox.com/",
+ "http://getthunderbird.com/",
+ ];
+ for (let url of arrayOfURLsToVisit) {
+ await PlacesTestUtils.addVisits(url);
+ }
+
+ // This tries to test fetchGuidForURL in two ways:
+ // 1- By fetching the GUID, and then using that GUID to retrieve the info of the visit.
+ // It then compares the URL with the URL that is on the visits info.
+ // 2- By creating a new GUID, changing the GUID for the visit, fetching the GUID and comparing them.
+ for (let url of arrayOfURLsToVisit) {
+ let guid = await PlacesSyncUtils.history.fetchGuidForURL(url);
+ let info = await PlacesSyncUtils.history.fetchURLInfoForGuid(guid);
+
+ let newGuid = makeGuid();
+ await PlacesSyncUtils.history.changeGuid(url, newGuid);
+ let newGuid2 = await PlacesSyncUtils.history.fetchGuidForURL(url);
+
+ equal(
+ url,
+ info.url,
+ "The url provided and the url retrieved should be the same."
+ );
+ equal(
+ newGuid,
+ newGuid2,
+ "The changed guid and the retrieved guid should be the same."
+ );
+ }
+
+ // Remove the visits added during this test.
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_fetchURLInfoForGuid() {
+ // Add some visits of the following URLs. specifying the title.
+ let visits = [
+ { uri: "https://www.mozilla.org/en-US/", title: "mozilla" },
+ { uri: "http://getfirefox.com/", title: "firefox" },
+ { uri: "http://getthunderbird.com/", title: "thunderbird" },
+ { uri: "http://quantum.mozilla.com/", title: null },
+ ];
+ for (let visit of visits) {
+ await PlacesTestUtils.addVisits(visit);
+ }
+
+ for (let visit of visits) {
+ let guid = await PlacesSyncUtils.history.fetchGuidForURL(visit.uri);
+ let info = await PlacesSyncUtils.history.fetchURLInfoForGuid(guid);
+
+ // Compare the info returned by fetchURLInfoForGuid,
+ // URL and title should match while frecency must be different than -1.
+ equal(
+ info.url,
+ visit.uri,
+ "The url provided should be the same as the url retrieved."
+ );
+ equal(
+ info.title,
+ visit.title || "",
+ "The title provided should be the same as the title retrieved."
+ );
+ notEqual(
+ info.frecency,
+ -1,
+ "The frecency of the visit should be different than -1."
+ );
+ }
+
+ // Create a "fake" GUID and check that the result of fetchURLInfoForGuid is null.
+ let guid = makeGuid();
+ let info = await PlacesSyncUtils.history.fetchURLInfoForGuid(guid);
+
+ equal(
+ info,
+ null,
+ "The information object of a non-existent guid should be null."
+ );
+
+ // Remove the visits added during this test.
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_getAllURLs() {
+ // Add some visits of the following URLs.
+ let arrayOfURLsToVisit = [
+ "https://www.mozilla.org/en-US/",
+ "http://getfirefox.com/",
+ "http://getthunderbird.com/",
+ ];
+ for (let url of arrayOfURLsToVisit) {
+ await PlacesTestUtils.addVisits(url);
+ }
+
+ // Get all URLs.
+ let allURLs = await PlacesSyncUtils.history.getAllURLs({
+ since: new Date(Date.now() - 2592000000),
+ limit: 5000,
+ });
+
+ // The amount of URLs must be the same in both collections.
+ equal(
+ allURLs.length,
+ arrayOfURLsToVisit.length,
+ "The amount of urls retrived should match the amount of urls provided."
+ );
+
+ // Check that the correct URLs were retrived.
+ for (let url of arrayOfURLsToVisit) {
+ ok(
+ allURLs.includes(url),
+ "The urls retrieved should match the ones used in this test."
+ );
+ }
+
+ // Remove the visits added during this test.
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_getAllURLs_skips_downloads() {
+ // Add some visits of the following URLs.
+ let arrayOfURLsToVisit = [
+ "https://www.mozilla.org/en-US/",
+ { uri: "http://downloads.com/", transition: TRANSITION_DOWNLOAD },
+ ];
+ for (let url of arrayOfURLsToVisit) {
+ await PlacesTestUtils.addVisits(url);
+ }
+
+ // Get all URLs.
+ let allURLs = await PlacesSyncUtils.history.getAllURLs({
+ since: new Date(Date.now() - 2592000000),
+ limit: 5000,
+ });
+
+ // Should be only the non-download
+ equal(allURLs.length, 1, "Should only get one URL back.");
+
+ // Check that the correct URLs were retrived.
+ equal(allURLs[0], arrayOfURLsToVisit[0], "Should get back our non-download.");
+
+ // Remove the visits added during this test.
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_order() {
+ info("Insert some bookmarks");
+ let guids = await populateTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ kind: "bookmark",
+ title: "childBmk",
+ url: "http://getfirefox.com",
+ },
+ {
+ kind: "bookmark",
+ title: "siblingBmk",
+ url: "http://getthunderbird.com",
+ },
+ {
+ kind: "folder",
+ title: "siblingFolder",
+ },
+ {
+ kind: "separator",
+ title: "siblingSep",
+ }
+ );
+
+ info("Reorder inserted bookmarks");
+ {
+ let order = [
+ guids.siblingFolder,
+ guids.siblingSep,
+ guids.childBmk,
+ guids.siblingBmk,
+ ];
+ await PlacesSyncUtils.bookmarks.order(
+ PlacesUtils.bookmarks.menuGuid,
+ order
+ );
+ let childRecordIds = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ deepEqual(
+ childRecordIds,
+ order,
+ "New bookmarks should be reordered according to array"
+ );
+ }
+
+ info("Same order with unspecified children");
+ {
+ await PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [
+ guids.siblingSep,
+ guids.siblingBmk,
+ ]);
+ let childRecordIds = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ deepEqual(
+ childRecordIds,
+ [guids.siblingFolder, guids.siblingSep, guids.childBmk, guids.siblingBmk],
+ "Current order should be respected if possible"
+ );
+ }
+
+ info("New order with unspecified children");
+ {
+ await PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [
+ guids.siblingBmk,
+ guids.siblingSep,
+ ]);
+ let childRecordIds = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ deepEqual(
+ childRecordIds,
+ [guids.siblingBmk, guids.siblingSep, guids.siblingFolder, guids.childBmk],
+ "Unordered children should be moved to end if current order can't be respected"
+ );
+ }
+
+ info("Reorder with nonexistent children");
+ {
+ await PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [
+ guids.childBmk,
+ makeGuid(),
+ guids.siblingBmk,
+ guids.siblingSep,
+ makeGuid(),
+ guids.siblingFolder,
+ makeGuid(),
+ ]);
+ let childRecordIds = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ deepEqual(
+ childRecordIds,
+ [guids.childBmk, guids.siblingBmk, guids.siblingSep, guids.siblingFolder],
+ "Nonexistent children should be ignored"
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_order_roots() {
+ let oldOrder = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
+ PlacesUtils.bookmarks.rootGuid
+ );
+ await PlacesSyncUtils.bookmarks.order(
+ PlacesUtils.bookmarks.rootGuid,
+ shuffle(oldOrder)
+ );
+ let newOrder = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
+ PlacesUtils.bookmarks.rootGuid
+ );
+ deepEqual(oldOrder, newOrder, "Should ignore attempts to reorder roots");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_pullChanges_tags() {
+ await ignoreChangedRoots();
+
+ info("Insert untagged items with same URL");
+ let firstItem = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ url: "https://example.org",
+ });
+ let secondItem = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ url: "https://example.org",
+ });
+ let untaggedItem = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ url: "https://bugzilla.org",
+ });
+ let taggedItem = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ url: "https://mozilla.org",
+ });
+
+ info("Create tag");
+ PlacesUtils.tagging.tagURI(uri("https://example.org"), ["taggy"]);
+
+ let tagBm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.tagsGuid,
+ index: 0,
+ });
+ let tagFolderGuid = tagBm.guid;
+ let tagFolderId = await PlacesUtils.promiseItemId(tagFolderGuid);
+
+ info("Tagged bookmarks should be in changeset");
+ {
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [firstItem.recordId, secondItem.recordId].sort(),
+ "Should include tagged bookmarks in changeset"
+ );
+ await setChangesSynced(changes);
+ }
+
+ info("Change tag case");
+ {
+ PlacesUtils.tagging.tagURI(uri("https://mozilla.org"), ["TaGgY"]);
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [firstItem.recordId, secondItem.recordId, taggedItem.recordId].sort(),
+ "Should include tagged bookmarks after changing case"
+ );
+ await assertTagForURLs(
+ "TaGgY",
+ ["https://example.org/", "https://mozilla.org/"],
+ "Should add tag for new URL"
+ );
+ await setChangesSynced(changes);
+ }
+
+ // These tests change a tag item directly, without going through the tagging
+ // service. This behavior isn't supported, but the tagging service registers
+ // an observer to handle these cases, so we make sure we handle them
+ // correctly.
+
+ info("Rename tag folder using Bookmarks.setItemTitle");
+ {
+ PlacesUtils.bookmarks.setItemTitle(tagFolderId, "sneaky");
+ deepEqual(
+ (await PlacesUtils.bookmarks.fetchTags()).map(t => t.name),
+ ["sneaky"],
+ "Tagging service should update cache with new title"
+ );
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [firstItem.recordId, secondItem.recordId].sort(),
+ "Should include tagged bookmarks after renaming tag folder"
+ );
+ await setChangesSynced(changes);
+ }
+
+ info("Rename tag folder using Bookmarks.update");
+ {
+ await PlacesUtils.bookmarks.update({
+ guid: tagFolderGuid,
+ title: "tricky",
+ });
+ deepEqual(
+ (await PlacesUtils.bookmarks.fetchTags()).map(t => t.name),
+ ["tricky"],
+ "Tagging service should update cache after updating tag folder"
+ );
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [firstItem.recordId, secondItem.recordId].sort(),
+ "Should include tagged bookmarks after updating tag folder"
+ );
+ await setChangesSynced(changes);
+ }
+
+ info("Change tag entry URL using Bookmarks.update");
+ {
+ let bm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: tagFolderGuid,
+ index: 0,
+ });
+ bm.url = "https://bugzilla.org/";
+ await PlacesUtils.bookmarks.update(bm);
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [firstItem.recordId, secondItem.recordId, untaggedItem.recordId].sort(),
+ "Should include tagged bookmarks after changing tag entry URI"
+ );
+ await assertTagForURLs(
+ "tricky",
+ ["https://bugzilla.org/", "https://mozilla.org/"],
+ "Should remove tag entry for old URI"
+ );
+ await setChangesSynced(changes);
+
+ bm.url = "https://example.org/";
+ await PlacesUtils.bookmarks.update(bm);
+ changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [firstItem.recordId, secondItem.recordId, untaggedItem.recordId].sort(),
+ "Should include tagged bookmarks after changing tag entry URL"
+ );
+ await assertTagForURLs(
+ "tricky",
+ ["https://example.org/", "https://mozilla.org/"],
+ "Should remove tag entry for old URL"
+ );
+ await setChangesSynced(changes);
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_conflicting_keywords() {
+ await ignoreChangedRoots();
+
+ info("Insert bookmark with new keyword");
+ let tbBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ recordId: makeGuid(),
+ parentRecordId: "unfiled",
+ url: "http://getthunderbird.com",
+ keyword: "tbird",
+ });
+ {
+ let entryByKeyword = await PlacesUtils.keywords.fetch("tbird");
+ equal(
+ entryByKeyword.url.href,
+ "http://getthunderbird.com/",
+ "Should return new keyword entry by URL"
+ );
+ let entryByURL = await PlacesUtils.keywords.fetch({
+ url: "http://getthunderbird.com",
+ });
+ equal(entryByURL.keyword, "tbird", "Should return new entry by keyword");
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ changes,
+ {},
+ "Should not bump change counter for new keyword entry"
+ );
+ }
+
+ info("Insert bookmark with same URL and different keyword");
+ let dupeTbBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ recordId: makeGuid(),
+ parentRecordId: "toolbar",
+ url: "http://getthunderbird.com",
+ keyword: "tb",
+ });
+ {
+ let oldKeywordByURL = await PlacesUtils.keywords.fetch("tbird");
+ ok(
+ !oldKeywordByURL,
+ "Should remove old entry when inserting bookmark with different keyword"
+ );
+ let entryByKeyword = await PlacesUtils.keywords.fetch("tb");
+ equal(
+ entryByKeyword.url.href,
+ "http://getthunderbird.com/",
+ "Should return different keyword entry by URL"
+ );
+ let entryByURL = await PlacesUtils.keywords.fetch({
+ url: "http://getthunderbird.com",
+ });
+ equal(entryByURL.keyword, "tb", "Should return different entry by keyword");
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [tbBmk.recordId, dupeTbBmk.recordId].sort(),
+ "Should bump change counter for bookmarks with different keyword"
+ );
+ await setChangesSynced(changes);
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_insert() {
+ info("Insert bookmark");
+ {
+ let item = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ url: "https://example.org",
+ });
+ let { type } = await PlacesUtils.bookmarks.fetch({ guid: item.recordId });
+ equal(
+ type,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "Bookmark should have correct type"
+ );
+ }
+
+ info("Insert query");
+ {
+ let item = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "query",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ url: "place:terms=term&folder=TOOLBAR&queryType=1",
+ folder: "Saved search",
+ });
+ let { type } = await PlacesUtils.bookmarks.fetch({ guid: item.recordId });
+ equal(
+ type,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "Queries should be stored as bookmarks"
+ );
+ }
+
+ info("Insert folder");
+ {
+ let item = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "folder",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ title: "New folder",
+ });
+ let { type } = await PlacesUtils.bookmarks.fetch({ guid: item.recordId });
+ equal(
+ type,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Folder should have correct type"
+ );
+ }
+
+ info("Insert separator");
+ {
+ let item = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "separator",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ });
+ let { type } = await PlacesUtils.bookmarks.fetch({ guid: item.recordId });
+ equal(
+ type,
+ PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ "Separator should have correct type"
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_insert_tags() {
+ await Promise.all(
+ [
+ {
+ kind: "bookmark",
+ url: "https://example.com",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ tags: ["foo", "bar"],
+ },
+ {
+ kind: "bookmark",
+ url: "https://example.org",
+ recordId: makeGuid(),
+ parentRecordId: "toolbar",
+ tags: ["foo", "baz"],
+ },
+ {
+ kind: "query",
+ url: "place:queryType=1&sort=12&maxResults=10",
+ recordId: makeGuid(),
+ parentRecordId: "toolbar",
+ folder: "bar",
+ tags: ["baz", "qux"],
+ title: "bar",
+ },
+ ].map(info => PlacesSyncUtils.test.bookmarks.insert(info))
+ );
+
+ await assertTagForURLs(
+ "foo",
+ ["https://example.com/", "https://example.org/"],
+ "2 URLs with new tag"
+ );
+ await assertTagForURLs(
+ "bar",
+ ["https://example.com/"],
+ "1 URL with existing tag"
+ );
+ await assertTagForURLs(
+ "baz",
+ ["https://example.org/", "place:queryType=1&sort=12&maxResults=10"],
+ "Should support tagging URLs and tag queries"
+ );
+ await assertTagForURLs(
+ "qux",
+ ["place:queryType=1&sort=12&maxResults=10"],
+ "Should support tagging tag queries"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_insert_tags_whitespace() {
+ info("Untrimmed and blank tags");
+ let taggedBlanks = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.org",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ tags: [" untrimmed ", " ", "taggy"],
+ });
+ deepEqual(
+ taggedBlanks.tags,
+ ["untrimmed", "taggy"],
+ "Should not return empty tags"
+ );
+ assertURLHasTags(
+ "https://example.org/",
+ ["taggy", "untrimmed"],
+ "Should set trimmed tags and ignore dupes"
+ );
+
+ info("Dupe tags");
+ let taggedDupes = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.net",
+ recordId: makeGuid(),
+ parentRecordId: "toolbar",
+ tags: [" taggy", "taggy ", " taggy ", "taggy"],
+ });
+ deepEqual(
+ taggedDupes.tags,
+ ["taggy", "taggy", "taggy", "taggy"],
+ "Should return trimmed and dupe tags"
+ );
+ assertURLHasTags(
+ "https://example.net/",
+ ["taggy"],
+ "Should ignore dupes when setting tags"
+ );
+
+ await assertTagForURLs(
+ "taggy",
+ ["https://example.net/", "https://example.org/"],
+ "Should exclude falsy tags"
+ );
+
+ PlacesUtils.tagging.untagURI(uri("https://example.org"), [
+ "untrimmed",
+ "taggy",
+ ]);
+ PlacesUtils.tagging.untagURI(uri("https://example.net"), ["taggy"]);
+ deepEqual(
+ (await PlacesUtils.bookmarks.fetchTags()).map(t => t.name),
+ [],
+ "Should clean up all tags"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_insert_keyword() {
+ info("Insert item with new keyword");
+ {
+ await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: "menu",
+ url: "https://example.com",
+ keyword: "moz",
+ recordId: makeGuid(),
+ });
+ let entry = await PlacesUtils.keywords.fetch("moz");
+ equal(
+ entry.url.href,
+ "https://example.com/",
+ "Should add keyword for item"
+ );
+ }
+
+ info("Insert item with existing keyword");
+ {
+ await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: "menu",
+ url: "https://mozilla.org",
+ keyword: "moz",
+ recordId: makeGuid(),
+ });
+ let entry = await PlacesUtils.keywords.fetch("moz");
+ equal(
+ entry.url.href,
+ "https://mozilla.org/",
+ "Should reassign keyword to new item"
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_insert_tag_query() {
+ info("Use the public tagging API to ensure we added the tag correctly");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "https://mozilla.org",
+ title: "Mozilla",
+ });
+ PlacesUtils.tagging.tagURI(uri("https://mozilla.org"), ["taggy"]);
+ assertURLHasTags(
+ "https://mozilla.org/",
+ ["taggy"],
+ "Should set tags using the tagging API"
+ );
+
+ info("Insert tag query for non existing tag");
+ {
+ let query = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "query",
+ recordId: makeGuid(),
+ parentRecordId: "toolbar",
+ url: "place:type=7&folder=90",
+ folder: "nonexisting",
+ title: "Tagged stuff",
+ });
+ let params = new URLSearchParams(query.url.pathname);
+ ok(!params.has("type"), "Should not preserve query type");
+ ok(!params.has("folder"), "Should not preserve folder");
+ equal(params.get("tag"), "nonexisting", "Should add tag");
+ deepEqual(
+ (await PlacesUtils.bookmarks.fetchTags()).map(t => t.name),
+ ["taggy"],
+ "The nonexisting tag should not be added"
+ );
+ }
+
+ info("Insert tag query for existing tag");
+ {
+ let url = "place:type=7&folder=90&maxResults=15";
+ let query = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "query",
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ url,
+ folder: "taggy",
+ title: "Sorted and tagged",
+ });
+ let params = new URLSearchParams(query.url.pathname);
+ ok(!params.get("type"), "Should not preserve query type");
+ ok(!params.has("folder"), "Should not preserve folder");
+ equal(params.get("maxResults"), "15", "Should preserve additional params");
+ equal(params.get("tag"), "taggy", "Should add tag");
+ deepEqual(
+ (await PlacesUtils.bookmarks.fetchTags()).map(t => t.name),
+ ["taggy"],
+ "Should not duplicate existing tags"
+ );
+ }
+
+ info("Removing the tag should clean up the tag folder");
+ PlacesUtils.tagging.untagURI(uri("https://mozilla.org"), null);
+ deepEqual(
+ (await PlacesUtils.bookmarks.fetchTags()).map(t => t.name),
+ [],
+ "Should remove tag folder once last item is untagged"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_fetch() {
+ let folder = await PlacesSyncUtils.test.bookmarks.insert({
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ kind: "folder",
+ });
+ let bmk = await PlacesSyncUtils.test.bookmarks.insert({
+ recordId: makeGuid(),
+ parentRecordId: "menu",
+ kind: "bookmark",
+ url: "https://example.com",
+ tags: ["taggy"],
+ });
+ let folderBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ recordId: makeGuid(),
+ parentRecordId: folder.recordId,
+ kind: "bookmark",
+ url: "https://example.org",
+ keyword: "kw",
+ });
+ let folderSep = await PlacesSyncUtils.test.bookmarks.insert({
+ recordId: makeGuid(),
+ parentRecordId: folder.recordId,
+ kind: "separator",
+ });
+ let tagQuery = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "query",
+ recordId: makeGuid(),
+ parentRecordId: "toolbar",
+ url: "place:tag=taggy",
+ folder: "taggy",
+ title: "Tagged stuff",
+ });
+
+ info("Fetch empty folder");
+ {
+ let item = await PlacesSyncUtils.bookmarks.fetch(folder.recordId);
+ deepEqual(
+ item,
+ {
+ recordId: folder.recordId,
+ kind: "folder",
+ parentRecordId: "menu",
+ childRecordIds: [folderBmk.recordId, folderSep.recordId],
+ parentTitle: "menu",
+ dateAdded: item.dateAdded,
+ title: "",
+ },
+ "Should include children, title, and parent title in folder"
+ );
+ }
+
+ info("Fetch bookmark with tags");
+ {
+ let item = await PlacesSyncUtils.bookmarks.fetch(bmk.recordId);
+ deepEqual(
+ Object.keys(item).sort(),
+ [
+ "recordId",
+ "kind",
+ "parentRecordId",
+ "url",
+ "tags",
+ "parentTitle",
+ "title",
+ "dateAdded",
+ ].sort(),
+ "Should include bookmark-specific properties"
+ );
+ equal(item.recordId, bmk.recordId, "Sync ID should match");
+ equal(item.url.href, "https://example.com/", "Should return URL");
+ equal(item.parentRecordId, "menu", "Should return parent sync ID");
+ deepEqual(item.tags, ["taggy"], "Should return tags");
+ equal(item.parentTitle, "menu", "Should return parent title");
+ strictEqual(item.title, "", "Should return empty title");
+ }
+
+ info("Fetch bookmark with keyword; without parent title");
+ {
+ let item = await PlacesSyncUtils.bookmarks.fetch(folderBmk.recordId);
+ deepEqual(
+ Object.keys(item).sort(),
+ [
+ "recordId",
+ "kind",
+ "parentRecordId",
+ "url",
+ "keyword",
+ "tags",
+ "parentTitle",
+ "title",
+ "dateAdded",
+ ].sort(),
+ "Should omit blank bookmark-specific properties"
+ );
+ deepEqual(item.tags, [], "Tags should be empty");
+ equal(item.keyword, "kw", "Should return keyword");
+ strictEqual(
+ item.parentTitle,
+ "",
+ "Should include parent title even if empty"
+ );
+ strictEqual(item.title, "", "Should include bookmark title even if empty");
+ }
+
+ info("Fetch separator");
+ {
+ let item = await PlacesSyncUtils.bookmarks.fetch(folderSep.recordId);
+ strictEqual(item.index, 1, "Should return separator position");
+ }
+
+ info("Fetch tag query");
+ {
+ let item = await PlacesSyncUtils.bookmarks.fetch(tagQuery.recordId);
+ deepEqual(
+ Object.keys(item).sort(),
+ [
+ "recordId",
+ "kind",
+ "parentRecordId",
+ "url",
+ "title",
+ "folder",
+ "parentTitle",
+ "dateAdded",
+ ].sort(),
+ "Should include query-specific properties"
+ );
+ equal(
+ item.url.href,
+ `place:tag=taggy`,
+ "Should not rewrite outgoing tag queries"
+ );
+ equal(item.folder, "taggy", "Should return tag name for tag queries");
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_pullChanges_new_parent() {
+ await ignoreChangedRoots();
+
+ let {
+ syncedGuids,
+ unsyncedFolder,
+ } = await moveSyncedBookmarksToUnsyncedParent();
+
+ info("Unsynced parent and synced items should be tracked");
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [
+ syncedGuids.folder,
+ syncedGuids.topBmk,
+ syncedGuids.childBmk,
+ unsyncedFolder.guid,
+ "menu",
+ ].sort(),
+ "Should return change records for moved items and new parent"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_pullChanges_deleted_folder() {
+ await ignoreChangedRoots();
+
+ let {
+ syncedGuids,
+ unsyncedFolder,
+ } = await moveSyncedBookmarksToUnsyncedParent();
+
+ info("Remove unsynced new folder");
+ await PlacesUtils.bookmarks.remove(unsyncedFolder.guid);
+
+ info("Deleted synced items should be tracked; unsynced folder should not");
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [
+ syncedGuids.folder,
+ syncedGuids.topBmk,
+ syncedGuids.childBmk,
+ "menu",
+ ].sort(),
+ "Should return change records for all deleted items"
+ );
+ for (let guid of Object.values(syncedGuids)) {
+ strictEqual(
+ changes[guid].tombstone,
+ true,
+ `Tombstone flag should be set for deleted item ${guid}`
+ );
+ equal(
+ changes[guid].counter,
+ 1,
+ `Change counter should be 1 for deleted item ${guid}`
+ );
+ equal(
+ changes[guid].status,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ `Sync status should be normal for deleted item ${guid}`
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_pullChanges_import_html() {
+ await ignoreChangedRoots();
+
+ info("Add unsynced bookmark");
+ let unsyncedBmk = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "https://example.com",
+ });
+
+ {
+ let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ unsyncedBmk.guid
+ );
+ ok(
+ fields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "Unsynced bookmark statuses should match"
+ );
+ }
+
+ info("Import new bookmarks from HTML");
+ let { path } = do_get_file("./sync_utils_bookmarks.html");
+ await BookmarkHTMLUtils.importFromFile(path);
+
+ // Bookmarks.html doesn't store IDs, so we need to look these up.
+ let mozBmk = await PlacesUtils.bookmarks.fetch({
+ url: "https://www.mozilla.org/",
+ });
+ let fxBmk = await PlacesUtils.bookmarks.fetch({
+ url: "https://www.mozilla.org/en-US/firefox/",
+ });
+ // All Bookmarks.html bookmarks are stored under the menu. For toolbar
+ // bookmarks, this means they're imported into a "Bookmarks Toolbar"
+ // subfolder under the menu, instead of the real toolbar root.
+ let toolbarSubfolder = (
+ await PlacesUtils.bookmarks.search({
+ title: "Bookmarks Toolbar",
+ })
+ ).find(item => item.guid != PlacesUtils.bookmarks.toolbarGuid);
+ let importedFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ mozBmk.guid,
+ fxBmk.guid,
+ toolbarSubfolder.guid
+ );
+ ok(
+ importedFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "Sync statuses should match for HTML imports"
+ );
+
+ info("Fetch new HTML imports");
+ let newChanges = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(newChanges).sort(),
+ [
+ mozBmk.guid,
+ fxBmk.guid,
+ toolbarSubfolder.guid,
+ "menu",
+ unsyncedBmk.guid,
+ ].sort(),
+ "Should return new IDs imported from HTML file"
+ );
+ let newFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ unsyncedBmk.guid,
+ mozBmk.guid,
+ fxBmk.guid,
+ toolbarSubfolder.guid
+ );
+ ok(
+ newFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "Pulling new HTML imports should not mark them as syncing"
+ );
+
+ info("Mark new HTML imports as syncing");
+ await PlacesSyncUtils.bookmarks.markChangesAsSyncing(newChanges);
+ let normalFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ unsyncedBmk.guid,
+ mozBmk.guid,
+ fxBmk.guid,
+ toolbarSubfolder.guid
+ );
+ ok(
+ normalFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ ),
+ "Marking new HTML imports as syncing should update their statuses"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_pullChanges_import_json() {
+ await ignoreChangedRoots();
+
+ info("Add synced folder");
+ let syncedFolder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "syncedFolder",
+ });
+ await PlacesTestUtils.setBookmarkSyncFields({
+ guid: syncedFolder.guid,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ });
+
+ info("Import new bookmarks from JSON");
+ let { path } = do_get_file("./sync_utils_bookmarks.json");
+ await BookmarkJSONUtils.importFromFile(path);
+ {
+ let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ syncedFolder.guid,
+ "NnvGl3CRA4hC",
+ "APzP8MupzA8l"
+ );
+ deepEqual(
+ fields.map(field => field.syncStatus),
+ [
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ ],
+ "Sync statuses should match for JSON imports"
+ );
+ }
+
+ info("Fetch new JSON imports");
+ let newChanges = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(newChanges).sort(),
+ [
+ "NnvGl3CRA4hC",
+ "APzP8MupzA8l",
+ "menu",
+ "toolbar",
+ syncedFolder.guid,
+ ].sort(),
+ "Should return items imported from JSON backup"
+ );
+ let existingFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ syncedFolder.guid,
+ "NnvGl3CRA4hC",
+ "APzP8MupzA8l"
+ );
+ deepEqual(
+ existingFields.map(field => field.syncStatus),
+ [
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ ],
+ "Pulling new JSON imports should not mark them as syncing"
+ );
+
+ info("Mark new JSON imports as syncing");
+ await PlacesSyncUtils.bookmarks.markChangesAsSyncing(newChanges);
+ let normalFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ syncedFolder.guid,
+ "NnvGl3CRA4hC",
+ "APzP8MupzA8l"
+ );
+ ok(
+ normalFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ ),
+ "Marking new JSON imports as syncing should update their statuses"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_pullChanges_restore_json_tracked() {
+ await ignoreChangedRoots();
+
+ let unsyncedBmk = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "https://example.com",
+ });
+ info(`Unsynced bookmark GUID: ${unsyncedBmk.guid}`);
+ let syncedFolder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "syncedFolder",
+ });
+ info(`Synced folder GUID: ${syncedFolder.guid}`);
+ await PlacesTestUtils.setBookmarkSyncFields({
+ guid: syncedFolder.guid,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ });
+ {
+ let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ unsyncedBmk.guid,
+ syncedFolder.guid
+ );
+ deepEqual(
+ fields.map(field => field.syncStatus),
+ [
+ PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ ],
+ "Sync statuses should match before restoring from JSON"
+ );
+ }
+
+ info("Restore from JSON, replacing existing items");
+ let { path } = do_get_file("./sync_utils_bookmarks.json");
+ await BookmarkJSONUtils.importFromFile(path, { replace: true });
+ {
+ let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ "NnvGl3CRA4hC",
+ "APzP8MupzA8l"
+ );
+ ok(
+ fields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "All bookmarks should be NEW after restoring from JSON"
+ );
+ }
+
+ info("Fetch new items restored from JSON");
+ {
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [
+ "menu",
+ "toolbar",
+ "unfiled",
+ "mobile",
+ "NnvGl3CRA4hC",
+ "APzP8MupzA8l",
+ ].sort(),
+ "Should restore items from JSON backup"
+ );
+
+ let existingFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+ "NnvGl3CRA4hC",
+ "APzP8MupzA8l"
+ );
+ ok(
+ existingFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "Items restored from JSON backup should not be marked as syncing"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones,
+ [],
+ "Tombstones should not exist after restoring from JSON backup"
+ );
+
+ await PlacesSyncUtils.bookmarks.markChangesAsSyncing(changes);
+ let normalFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ "NnvGl3CRA4hC",
+ "APzP8MupzA8l"
+ );
+ ok(
+ normalFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ ),
+ "Roots and NEW items restored from JSON backup should be marked as NORMAL"
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_pullChanges_tombstones() {
+ await ignoreChangedRoots();
+
+ info("Insert new bookmarks");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ url: "http://example.com/a",
+ title: "A",
+ },
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ ],
+ });
+
+ info("Manually insert conflicting tombstone for new bookmark");
+ await PlacesUtils.withConnectionWrapper(
+ "test_pullChanges_tombstones",
+ async function(db) {
+ await db.executeCached(
+ `
+ INSERT INTO moz_bookmarks_deleted(guid)
+ VALUES(:guid)`,
+ { guid: "bookmarkAAAA" }
+ );
+ }
+ );
+
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ ["bookmarkAAAA", "bookmarkBBBB", "menu"],
+ "Should handle undeleted items when returning changes"
+ );
+ strictEqual(
+ changes.bookmarkAAAA.tombstone,
+ false,
+ "Should replace tombstone for A with undeleted item"
+ );
+ strictEqual(
+ changes.bookmarkBBBB.tombstone,
+ false,
+ "Should not report B as deleted"
+ );
+
+ await setChangesSynced(changes);
+
+ let newChanges = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ newChanges,
+ {},
+ "Should not return changes after marking undeleted items as synced"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_pushChanges() {
+ await ignoreChangedRoots();
+
+ info("Populate test bookmarks");
+ let guids = await populateTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ kind: "bookmark",
+ title: "unknownBmk",
+ url: "https://example.org",
+ },
+ {
+ kind: "bookmark",
+ title: "syncedBmk",
+ url: "https://example.com",
+ },
+ {
+ kind: "bookmark",
+ title: "newBmk",
+ url: "https://example.info",
+ },
+ {
+ kind: "bookmark",
+ title: "deletedBmk",
+ url: "https://example.edu",
+ },
+ {
+ kind: "bookmark",
+ title: "unchangedBmk",
+ url: "https://example.systems",
+ }
+ );
+
+ info("Update sync statuses");
+ await PlacesTestUtils.setBookmarkSyncFields(
+ {
+ guid: guids.syncedBmk,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ },
+ {
+ guid: guids.unknownBmk,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN,
+ },
+ {
+ guid: guids.deletedBmk,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ },
+ {
+ guid: guids.unchangedBmk,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ syncChangeCounter: 0,
+ }
+ );
+
+ info("Change synced bookmark; should bump change counter");
+ await PlacesUtils.bookmarks.update({
+ guid: guids.syncedBmk,
+ url: "https://example.ninja",
+ });
+
+ info("Remove synced bookmark");
+ {
+ await PlacesUtils.bookmarks.remove(guids.deletedBmk);
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ ok(
+ tombstones.some(({ guid }) => guid == guids.deletedBmk),
+ "Should write tombstone for deleted synced bookmark"
+ );
+ }
+
+ info("Pull changes");
+ let totalSyncChanges = PlacesUtils.bookmarks.totalSyncChanges;
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ {
+ let actualChanges = Object.entries(changes).map(([recordId, change]) => ({
+ recordId,
+ syncChangeCounter: change.counter,
+ }));
+ let expectedChanges = [
+ {
+ recordId: guids.unknownBmk,
+ syncChangeCounter: 1,
+ },
+ {
+ // Parent of changed bookmarks.
+ recordId: "menu",
+ syncChangeCounter: 6,
+ },
+ {
+ recordId: guids.syncedBmk,
+ syncChangeCounter: 2,
+ },
+ {
+ recordId: guids.newBmk,
+ syncChangeCounter: 1,
+ },
+ {
+ recordId: guids.deletedBmk,
+ syncChangeCounter: 1,
+ },
+ ];
+ deepEqual(
+ sortBy(actualChanges, "recordId"),
+ sortBy(expectedChanges, "recordId"),
+ "Should return deleted, new, and unknown bookmarks"
+ );
+ }
+
+ info("Modify changed bookmark to bump its counter");
+ await PlacesUtils.bookmarks.update({
+ guid: guids.newBmk,
+ url: "https://example.club",
+ });
+
+ info("Mark some bookmarks as synced");
+ for (let title of ["unknownBmk", "newBmk", "deletedBmk"]) {
+ let guid = guids[title];
+ strictEqual(
+ changes[guid].synced,
+ false,
+ "All bookmarks should not be marked as synced yet"
+ );
+ changes[guid].synced = true;
+ }
+
+ await PlacesSyncUtils.bookmarks.pushChanges(changes);
+ equal(PlacesUtils.bookmarks.totalSyncChanges, totalSyncChanges + 4);
+
+ {
+ let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ guids.newBmk,
+ guids.unknownBmk
+ );
+ ok(
+ fields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ ),
+ "Should update sync statuses for synced bookmarks"
+ );
+ }
+
+ {
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ ok(
+ !tombstones.some(({ guid }) => guid == guids.deletedBmk),
+ "Should remove tombstone after syncing"
+ );
+
+ let syncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ guids.unknownBmk,
+ guids.syncedBmk,
+ guids.newBmk
+ );
+ {
+ let info = syncFields.find(field => field.guid == guids.unknownBmk);
+ equal(
+ info.syncStatus,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ "Syncing an UNKNOWN bookmark should set its sync status to NORMAL"
+ );
+ strictEqual(
+ info.syncChangeCounter,
+ 0,
+ "Syncing an UNKNOWN bookmark should reduce its change counter"
+ );
+ }
+ {
+ let info = syncFields.find(field => field.guid == guids.syncedBmk);
+ equal(
+ info.syncStatus,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ "Syncing a NORMAL bookmark should not update its sync status"
+ );
+ equal(
+ info.syncChangeCounter,
+ 2,
+ "Should not reduce counter for NORMAL bookmark not marked as synced"
+ );
+ }
+ {
+ let info = syncFields.find(field => field.guid == guids.newBmk);
+ equal(
+ info.syncStatus,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ "Syncing a NEW bookmark should update its sync status"
+ );
+ strictEqual(
+ info.syncChangeCounter,
+ 1,
+ "Updating new bookmark after pulling changes should bump change counter"
+ );
+ }
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_changes_between_pull_and_push() {
+ await ignoreChangedRoots();
+
+ info("Populate test bookmarks");
+ let guids = await populateTree(PlacesUtils.bookmarks.menuGuid, {
+ kind: "bookmark",
+ title: "bmk",
+ url: "https://example.info",
+ });
+
+ info("Update sync statuses");
+ await PlacesTestUtils.setBookmarkSyncFields({
+ guid: guids.bmk,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ syncChangeCounter: 1,
+ });
+
+ info("Pull changes");
+ let totalSyncChanges = PlacesUtils.bookmarks.totalSyncChanges;
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ Assert.equal(changes[guids.bmk].counter, 1);
+ Assert.equal(changes[guids.bmk].tombstone, false);
+
+ // delete the bookmark.
+ await PlacesUtils.bookmarks.remove(guids.bmk);
+
+ info("Push changes");
+ await PlacesSyncUtils.bookmarks.pushChanges(changes);
+ equal(PlacesUtils.bookmarks.totalSyncChanges, totalSyncChanges + 2);
+
+ // we should have a tombstone.
+ let ts = await PlacesTestUtils.fetchSyncTombstones();
+ Assert.equal(ts.length, 1);
+ Assert.equal(ts[0].guid, guids.bmk);
+
+ // there should be no record for the item we deleted.
+ Assert.strictEqual(await PlacesUtils.bookmarks.fetch(guids.bmk), null);
+
+ // and re-fetching changes should list it as a tombstone.
+ changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ Assert.equal(changes[guids.bmk].counter, 1);
+ Assert.equal(changes[guids.bmk].tombstone, true);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_separator() {
+ await ignoreChangedRoots();
+
+ await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: "menu",
+ recordId: makeGuid(),
+ url: "https://example.com",
+ });
+ let childBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: "menu",
+ recordId: makeGuid(),
+ url: "https://foo.bar",
+ });
+ let separatorRecordId = makeGuid();
+ let separator = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "separator",
+ parentRecordId: "menu",
+ recordId: separatorRecordId,
+ });
+ await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: "menu",
+ recordId: makeGuid(),
+ url: "https://bar.foo",
+ });
+
+ let child2Guid = await PlacesSyncUtils.bookmarks.recordIdToGuid(
+ childBmk.recordId
+ );
+ let parentGuid = await await PlacesSyncUtils.bookmarks.recordIdToGuid("menu");
+ let separatorGuid = PlacesSyncUtils.bookmarks.recordIdToGuid(
+ separatorRecordId
+ );
+
+ info("Move a bookmark around the separator");
+ await PlacesUtils.bookmarks.update({
+ guid: child2Guid,
+ parentGuid,
+ index: 2,
+ });
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(Object.keys(changes).sort(), [separator.recordId, "menu"].sort());
+
+ await setChangesSynced(changes);
+
+ info("Move a separator around directly");
+ await PlacesUtils.bookmarks.update({
+ guid: separatorGuid,
+ parentGuid,
+ index: 0,
+ });
+
+ changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(Object.keys(changes).sort(), [separator.recordId, "menu"].sort());
+
+ await setChangesSynced(changes);
+
+ info("Move a separator around directly using update");
+ await PlacesUtils.bookmarks.update({ guid: separatorGuid, index: 2 });
+ changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(Object.keys(changes).sort(), [separator.recordId, "menu"].sort());
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_remove() {
+ await ignoreChangedRoots();
+
+ info("Insert subtree for removal");
+ let parentFolder = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "folder",
+ parentRecordId: "menu",
+ recordId: makeGuid(),
+ });
+ let childBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: parentFolder.recordId,
+ recordId: makeGuid(),
+ url: "https://example.com",
+ });
+ let childFolder = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "folder",
+ parentRecordId: parentFolder.recordId,
+ recordId: makeGuid(),
+ });
+ let grandChildBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: childFolder.recordId,
+ recordId: makeGuid(),
+ url: "https://example.edu",
+ });
+
+ info("Remove entire subtree");
+ await PlacesSyncUtils.bookmarks.remove([
+ parentFolder.recordId,
+ childFolder.recordId,
+ childBmk.recordId,
+ grandChildBmk.recordId,
+ ]);
+
+ /**
+ * Even though we've removed the entire subtree, we still track the menu
+ * because we 1) removed `parentFolder`, 2) reparented `childFolder` to
+ * `menu`, and 3) removed `childFolder`.
+ *
+ * This depends on the order of the folders passed to `remove`. If we
+ * removed `childFolder` *before* `parentFolder`, we wouldn't reparent
+ * anything to `menu`.
+ *
+ * `deleteSyncedFolder` could check if it's reparenting an item that will
+ * eventually be removed, and avoid bumping the new parent's change counter.
+ * Unfortunately, that introduces inconsistencies if `deleteSyncedFolder` is
+ * interrupted by shutdown. If the server changes before the next sync,
+ * we'll never upload records for the reparented item or the new parent.
+ *
+ * Another alternative: we can try to remove folders in level order, instead
+ * of the order passed to `remove`. But that means we need a recursive query
+ * to determine the order. This is already enough of an edge case that
+ * occasionally reuploading the closest living ancestor is the simplest
+ * solution.
+ */
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes),
+ ["menu"],
+ "Should track closest living ancestor of removed subtree"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_remove_partial() {
+ await ignoreChangedRoots();
+
+ info("Insert subtree for partial removal");
+ let parentFolder = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "folder",
+ parentRecordId: PlacesUtils.bookmarks.menuGuid,
+ recordId: makeGuid(),
+ });
+ let prevSiblingBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: parentFolder.recordId,
+ recordId: makeGuid(),
+ url: "https://example.net",
+ });
+ let childBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: parentFolder.recordId,
+ recordId: makeGuid(),
+ url: "https://example.com",
+ });
+ let nextSiblingBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: parentFolder.recordId,
+ recordId: makeGuid(),
+ url: "https://example.org",
+ });
+ let childFolder = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "folder",
+ parentRecordId: parentFolder.recordId,
+ recordId: makeGuid(),
+ });
+ let grandChildBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: parentFolder.recordId,
+ recordId: makeGuid(),
+ url: "https://example.edu",
+ });
+ let grandChildSiblingBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: parentFolder.recordId,
+ recordId: makeGuid(),
+ url: "https://mozilla.org",
+ });
+ let grandChildFolder = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "folder",
+ parentRecordId: childFolder.recordId,
+ recordId: makeGuid(),
+ });
+ let greatGrandChildPrevSiblingBmk = await PlacesSyncUtils.test.bookmarks.insert(
+ {
+ kind: "bookmark",
+ parentRecordId: grandChildFolder.recordId,
+ recordId: makeGuid(),
+ url: "http://getfirefox.com",
+ }
+ );
+ let greatGrandChildNextSiblingBmk = await PlacesSyncUtils.test.bookmarks.insert(
+ {
+ kind: "bookmark",
+ parentRecordId: grandChildFolder.recordId,
+ recordId: makeGuid(),
+ url: "http://getthunderbird.com",
+ }
+ );
+ let menuBmk = await PlacesSyncUtils.test.bookmarks.insert({
+ kind: "bookmark",
+ parentRecordId: "menu",
+ recordId: makeGuid(),
+ url: "https://example.info",
+ });
+
+ info("Remove subset of folders and items in subtree");
+ let changes = await PlacesSyncUtils.bookmarks.remove([
+ parentFolder.recordId,
+ childBmk.recordId,
+ grandChildFolder.recordId,
+ grandChildBmk.recordId,
+ childFolder.recordId,
+ ]);
+ deepEqual(
+ Object.keys(changes).sort(),
+ [
+ // Closest living ancestor.
+ "menu",
+ // Reparented bookmarks.
+ prevSiblingBmk.recordId,
+ nextSiblingBmk.recordId,
+ grandChildSiblingBmk.recordId,
+ greatGrandChildPrevSiblingBmk.recordId,
+ greatGrandChildNextSiblingBmk.recordId,
+ ].sort(),
+ "Should track reparented bookmarks and their closest living ancestor"
+ );
+
+ /**
+ * Reparented bookmarks should maintain their order relative to their
+ * siblings: `prevSiblingBmk` (0) should precede `nextSiblingBmk` (2) in the
+ * menu, and `greatGrandChildPrevSiblingBmk` (0) should precede
+ * `greatGrandChildNextSiblingBmk` (1).
+ */
+ let menuChildren = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ deepEqual(
+ menuChildren,
+ [
+ // Existing bookmark.
+ menuBmk.recordId,
+ // 1) Moved out of `parentFolder` to `menu`.
+ prevSiblingBmk.recordId,
+ nextSiblingBmk.recordId,
+ // 3) Moved out of `childFolder` to `menu`. After this step, `childFolder`
+ // is deleted.
+ grandChildSiblingBmk.recordId,
+ // 2) Moved out of `grandChildFolder` to `childFolder`, because we remove
+ // `grandChildFolder` *before* `childFolder`. After this step,
+ // `grandChildFolder` is deleted and `childFolder`'s children are
+ // `[grandChildSiblingBmk, greatGrandChildPrevSiblingBmk,
+ // greatGrandChildNextSiblingBmk]`.
+ greatGrandChildPrevSiblingBmk.recordId,
+ greatGrandChildNextSiblingBmk.recordId,
+ ],
+ "Should move descendants to closest living ancestor"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_migrateOldTrackerEntries() {
+ let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
+ Preferences.set("privacy.reduceTimerPrecision", false);
+
+ registerCleanupFunction(function() {
+ Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
+ });
+
+ let unknownBmk = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://getfirefox.com",
+ title: "Get Firefox!",
+ });
+ let newBmk = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://getthunderbird.com",
+ title: "Get Thunderbird!",
+ });
+ let normalBmk = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "https://mozilla.org",
+ title: "Mozilla",
+ });
+
+ await PlacesTestUtils.setBookmarkSyncFields(
+ {
+ guid: unknownBmk.guid,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN,
+ syncChangeCounter: 0,
+ },
+ {
+ guid: normalBmk.guid,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ }
+ );
+ PlacesUtils.tagging.tagURI(uri("http://getfirefox.com"), ["taggy"]);
+
+ let tombstoneRecordId = makeGuid();
+ await PlacesSyncUtils.bookmarks.migrateOldTrackerEntries([
+ {
+ recordId: normalBmk.guid,
+ modified: Date.now(),
+ },
+ {
+ recordId: tombstoneRecordId,
+ modified: 1479162463976,
+ },
+ ]);
+
+ let changes = await PlacesSyncUtils.bookmarks.pullChanges();
+ deepEqual(
+ Object.keys(changes).sort(),
+ [normalBmk.guid, tombstoneRecordId].sort(),
+ "Should return change records for migrated bookmark and tombstone"
+ );
+
+ let fields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ unknownBmk.guid,
+ newBmk.guid,
+ normalBmk.guid
+ );
+ for (let field of fields) {
+ if (field.guid == normalBmk.guid) {
+ ok(
+ field.lastModified > normalBmk.lastModified,
+ `Should bump last modified date for migrated bookmark ${field.guid}`
+ );
+ equal(
+ field.syncChangeCounter,
+ 1,
+ `Should bump change counter for migrated bookmark ${field.guid}`
+ );
+ } else {
+ strictEqual(
+ field.syncChangeCounter,
+ 0,
+ `Should not bump change counter for ${field.guid}`
+ );
+ }
+ equal(
+ field.syncStatus,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ `Should set sync status for ${field.guid} to NORMAL`
+ );
+ }
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones,
+ [
+ {
+ guid: tombstoneRecordId,
+ dateRemoved: new Date(1479162463976),
+ },
+ ],
+ "Should write tombstone for nonexistent migrated item"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_ensureMobileQuery() {
+ info("Ensure we correctly set the showMobileBookmarks preference");
+ const mobilePref = "browser.bookmarks.showMobileBookmarks";
+ Services.prefs.clearUserPref(mobilePref);
+
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkAAAA",
+ parentGuid: PlacesUtils.bookmarks.mobileGuid,
+ url: "http://example.com/a",
+ title: "A",
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkBBBB",
+ parentGuid: PlacesUtils.bookmarks.mobileGuid,
+ url: "http://example.com/b",
+ title: "B",
+ });
+
+ await PlacesSyncUtils.bookmarks.ensureMobileQuery();
+
+ Assert.ok(
+ Services.prefs.getBoolPref(mobilePref),
+ "Pref should be true where there are bookmarks in the folder."
+ );
+
+ await PlacesUtils.bookmarks.remove("bookmarkAAAA");
+ await PlacesUtils.bookmarks.remove("bookmarkBBBB");
+
+ await PlacesSyncUtils.bookmarks.ensureMobileQuery();
+
+ Assert.ok(
+ !Services.prefs.getBoolPref(mobilePref),
+ "Pref should be false where there are no bookmarks in the folder."
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_remove_stale_tombstones() {
+ info("Insert and delete synced bookmark");
+ {
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkAAAA",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://example.com/a",
+ title: "A",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ });
+ await PlacesUtils.bookmarks.remove("bookmarkAAAA");
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["bookmarkAAAA"],
+ "Should store tombstone for deleted synced bookmark"
+ );
+ }
+
+ info("Reinsert deleted bookmark");
+ {
+ // Different parent, URL, and title, but same GUID.
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkAAAA",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/a-restored",
+ title: "A (Restored)",
+ });
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones,
+ [],
+ "Should remove tombstone for reinserted bookmark"
+ );
+ }
+
+ info("Insert tree and erase everything");
+ {
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.eraseEverything();
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid).sort(),
+ ["bookmarkBBBB", "bookmarkCCCC"],
+ "Should store tombstones after erasing everything"
+ );
+ }
+
+ info("Reinsert tree");
+ {
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ },
+ ],
+ });
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid).sort(),
+ [],
+ "Should remove tombstones after reinserting tree"
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_bookmarks_resetSyncId() {
+ let syncId = await PlacesSyncUtils.bookmarks.getSyncId();
+ strictEqual(syncId, "", "Should start with empty bookmarks sync ID");
+
+ // Add a tree with a NORMAL bookmark (A), tombstone (B), NEW bookmark (C),
+ // and UNKNOWN bookmark (D).
+ info("Set up local tree before resetting bookmarks sync ID");
+ await ignoreChangedRoots();
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "bookmarkBBBB",
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.remove("bookmarkBBBB");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ guid: "bookmarkCCCC",
+ title: "C",
+ url: "http://example.com/c",
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ guid: "bookmarkDDDD",
+ title: "D",
+ url: "http://example.com/d",
+ source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+ });
+
+ info("Assign new bookmarks sync ID for first time");
+ let newSyncId = await PlacesSyncUtils.bookmarks.resetSyncId();
+ syncId = await PlacesSyncUtils.bookmarks.getSyncId();
+ equal(
+ newSyncId,
+ syncId,
+ "Should assign new bookmarks sync ID for first time"
+ );
+
+ let syncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ "bookmarkAAAA",
+ "bookmarkCCCC",
+ "bookmarkDDDD"
+ );
+ ok(
+ syncFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "Should change all sync statuses to NEW after resetting bookmarks sync ID"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones,
+ [],
+ "Should remove all tombstones after resetting bookmarks sync ID"
+ );
+
+ info("Set bookmarks last sync time");
+ let lastSync = Date.now() / 1000;
+ await PlacesSyncUtils.bookmarks.setLastSync(lastSync);
+ equal(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ lastSync,
+ "Should record bookmarks last sync time"
+ );
+
+ newSyncId = await PlacesSyncUtils.bookmarks.resetSyncId();
+ notEqual(
+ newSyncId,
+ syncId,
+ "Should set new bookmarks sync ID if one already exists"
+ );
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ 0,
+ "Should reset bookmarks last sync time after resetting sync ID"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_bookmarks_wipe() {
+ info("Add Sync metadata before wipe");
+ let newSyncId = await PlacesSyncUtils.bookmarks.resetSyncId();
+ await PlacesSyncUtils.bookmarks.setLastSync(Date.now() / 1000);
+
+ let existingSyncId = await PlacesSyncUtils.bookmarks.getSyncId();
+ equal(
+ existingSyncId,
+ newSyncId,
+ "Ensure bookmarks sync ID was recorded before wipe"
+ );
+
+ info("Set up local tree before wipe");
+ await ignoreChangedRoots();
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "bookmarkBBBB",
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.remove("bookmarkBBBB");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ guid: "bookmarkCCCC",
+ title: "C",
+ url: "http://example.com/c",
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ guid: "bookmarkDDDD",
+ title: "D",
+ url: "http://example.com/d",
+ source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+ });
+
+ info("Wipe bookmarks");
+ await PlacesSyncUtils.bookmarks.wipe();
+
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getSyncId(),
+ "",
+ "Should reset bookmarks sync ID after wipe"
+ );
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ 0,
+ "Should reset bookmarks last sync after wipe"
+ );
+ ok(
+ !(await PlacesSyncUtils.bookmarks.shouldWipeRemote()),
+ "Wiping bookmarks locally should not wipe server"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(tombstones, [], "Should drop tombstones after wipe");
+
+ deepEqual(
+ await PlacesSyncUtils.bookmarks.fetchChildRecordIds("menu"),
+ [],
+ "Should wipe menu children"
+ );
+ deepEqual(
+ await PlacesSyncUtils.bookmarks.fetchChildRecordIds("toolbar"),
+ [],
+ "Should wipe toolbar children"
+ );
+
+ let rootSyncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ ok(
+ rootSyncFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "Should reset all sync statuses to NEW after wipe"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_bookmarks_meta_eraseEverything() {
+ info("Add Sync metadata before erase");
+ let newSyncId = await PlacesSyncUtils.bookmarks.resetSyncId();
+ let lastSync = Date.now() / 1000;
+ await PlacesSyncUtils.bookmarks.setLastSync(lastSync);
+
+ info("Set up local tree before reset");
+ await ignoreChangedRoots();
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "bookmarkBBBB",
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.remove("bookmarkBBBB");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ guid: "bookmarkCCCC",
+ title: "C",
+ url: "http://example.com/c",
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ guid: "bookmarkDDDD",
+ title: "D",
+ url: "http://example.com/d",
+ source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+ });
+
+ info("Erase all bookmarks");
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getSyncId(),
+ newSyncId,
+ "Should not reset bookmarks sync ID after erase"
+ );
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ lastSync,
+ "Should not reset bookmarks last sync after erase"
+ );
+ ok(
+ !(await PlacesSyncUtils.bookmarks.shouldWipeRemote()),
+ "Erasing everything should not wipe server"
+ );
+
+ deepEqual(
+ (await PlacesTestUtils.fetchSyncTombstones()).map(info => info.guid),
+ ["bookmarkAAAA", "bookmarkBBBB"],
+ "Should keep tombstones after erasing everything"
+ );
+
+ let rootSyncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.mobileGuid
+ );
+ ok(
+ rootSyncFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NORMAL
+ ),
+ "Should not reset sync statuses after erasing everything"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_bookmarks_reset() {
+ info("Add Sync metadata before reset");
+ await PlacesSyncUtils.bookmarks.resetSyncId();
+ await PlacesSyncUtils.bookmarks.setLastSync(Date.now() / 1000);
+
+ info("Set up local tree before reset");
+ await ignoreChangedRoots();
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "bookmarkBBBB",
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.remove("bookmarkBBBB");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ guid: "bookmarkCCCC",
+ title: "C",
+ url: "http://example.com/c",
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ guid: "bookmarkDDDD",
+ title: "D",
+ url: "http://example.com/d",
+ source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+ });
+
+ info("Reset Sync metadata for bookmarks");
+ await PlacesSyncUtils.bookmarks.reset();
+
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getSyncId(),
+ "",
+ "Should reset bookmarks sync ID after reset"
+ );
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ 0,
+ "Should reset bookmarks last sync after reset"
+ );
+ ok(
+ !(await PlacesSyncUtils.bookmarks.shouldWipeRemote()),
+ "Resetting Sync metadata should not wipe server"
+ );
+
+ deepEqual(
+ await PlacesTestUtils.fetchSyncTombstones(),
+ [],
+ "Should drop tombstones after reset"
+ );
+
+ let itemSyncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ "bookmarkAAAA",
+ "bookmarkCCCC"
+ );
+ ok(
+ itemSyncFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "Should reset sync statuses for existing items to NEW after reset"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_bookmarks_meta_restore() {
+ info("Add Sync metadata before manual restore");
+ await PlacesSyncUtils.bookmarks.resetSyncId();
+ await PlacesSyncUtils.bookmarks.setLastSync(Date.now() / 1000);
+
+ ok(
+ !(await PlacesSyncUtils.bookmarks.shouldWipeRemote()),
+ "Should not wipe server before manual restore"
+ );
+
+ info("Manually restore");
+ let { path } = do_get_file("./sync_utils_bookmarks.json");
+ await BookmarkJSONUtils.importFromFile(path, { replace: true });
+
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getSyncId(),
+ "",
+ "Should reset bookmarks sync ID after manual restore"
+ );
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ 0,
+ "Should reset bookmarks last sync after manual restore"
+ );
+ ok(
+ await PlacesSyncUtils.bookmarks.shouldWipeRemote(),
+ "Should wipe server after manual restore"
+ );
+
+ let syncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ "NnvGl3CRA4hC",
+ PlacesUtils.bookmarks.toolbarGuid,
+ "APzP8MupzA8l"
+ );
+ ok(
+ syncFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.NEW
+ ),
+ "Should reset all sync stauses to NEW after manual restore"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_bookmarks_meta_restore_on_startup() {
+ info("Add Sync metadata before simulated automatic restore");
+ await PlacesSyncUtils.bookmarks.resetSyncId();
+ await PlacesSyncUtils.bookmarks.setLastSync(Date.now() / 1000);
+
+ ok(
+ !(await PlacesSyncUtils.bookmarks.shouldWipeRemote()),
+ "Should not wipe server before automatic restore"
+ );
+
+ info("Simulate automatic restore on startup");
+ let { path } = do_get_file("./sync_utils_bookmarks.json");
+ await BookmarkJSONUtils.importFromFile(path, {
+ replace: true,
+ source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+ });
+
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getSyncId(),
+ "",
+ "Should reset bookmarks sync ID after automatic restore"
+ );
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ 0,
+ "Should reset bookmarks last sync after automatic restore"
+ );
+ ok(
+ !(await PlacesSyncUtils.bookmarks.shouldWipeRemote()),
+ "Should not wipe server after manual restore"
+ );
+
+ let syncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ "NnvGl3CRA4hC",
+ PlacesUtils.bookmarks.toolbarGuid,
+ "APzP8MupzA8l"
+ );
+ ok(
+ syncFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN
+ ),
+ "Should reset all sync stauses to UNKNOWN after automatic restore"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_bookmarks_ensureCurrentSyncId() {
+ info("Set up local tree");
+ await ignoreChangedRoots();
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "bookmarkBBBB",
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.remove("bookmarkBBBB");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ guid: "bookmarkCCCC",
+ title: "C",
+ url: "http://example.com/c",
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ guid: "bookmarkDDDD",
+ title: "D",
+ url: "http://example.com/d",
+ source: PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
+ });
+
+ let existingSyncId = await PlacesSyncUtils.bookmarks.getSyncId();
+ strictEqual(existingSyncId, "", "Should start without bookmarks sync ID");
+
+ info("Assign new bookmarks sync ID");
+ {
+ await PlacesSyncUtils.bookmarks.ensureCurrentSyncId("syncIdAAAAAA");
+
+ let newSyncId = await PlacesSyncUtils.bookmarks.getSyncId();
+ equal(
+ newSyncId,
+ "syncIdAAAAAA",
+ "Should assign bookmarks sync ID if one doesn't exist"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["bookmarkBBBB"],
+ "Should keep tombstones after assigning new bookmarks sync ID"
+ );
+
+ let syncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ "bookmarkAAAA",
+ "bookmarkCCCC",
+ "bookmarkDDDD"
+ );
+ deepEqual(
+ syncFields.map(field => field.syncStatus),
+ [
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN,
+ ],
+ "Should not reset sync statuses after assigning new bookmarks sync ID"
+ );
+ }
+
+ info("Ensure existing bookmarks sync ID matches");
+ {
+ let lastSync = Date.now() / 1000;
+ await PlacesSyncUtils.bookmarks.setLastSync(lastSync);
+ await PlacesSyncUtils.bookmarks.ensureCurrentSyncId("syncIdAAAAAA");
+
+ equal(
+ await PlacesSyncUtils.bookmarks.getSyncId(),
+ "syncIdAAAAAA",
+ "Should keep existing bookmarks sync ID on match"
+ );
+ equal(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ lastSync,
+ "Should keep existing bookmarks last sync time on sync ID match"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["bookmarkBBBB"],
+ "Should keep tombstones if bookmarks sync IDs match"
+ );
+
+ let syncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ "bookmarkAAAA",
+ "bookmarkCCCC",
+ "bookmarkDDDD"
+ );
+ deepEqual(
+ syncFields.map(field => field.syncStatus),
+ [
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NORMAL,
+ PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN,
+ ],
+ "Should not reset sync statuses if bookmarks sync IDs match"
+ );
+ }
+
+ info("Replace existing bookmarks sync ID with new ID");
+ {
+ await PlacesSyncUtils.bookmarks.ensureCurrentSyncId("syncIdBBBBBB");
+
+ equal(
+ await PlacesSyncUtils.bookmarks.getSyncId(),
+ "syncIdBBBBBB",
+ "Should replace existing bookmarks sync ID on mismatch"
+ );
+ strictEqual(
+ await PlacesSyncUtils.bookmarks.getLastSync(),
+ 0,
+ "Should reset bookmarks last sync time on sync ID mismatch"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones,
+ [],
+ "Should drop tombstones after bookmarks sync ID mismatch"
+ );
+
+ let syncFields = await PlacesTestUtils.fetchBookmarkSyncFields(
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ "bookmarkAAAA",
+ "bookmarkCCCC",
+ "bookmarkDDDD"
+ );
+ ok(
+ syncFields.every(
+ field => field.syncStatus == PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN
+ ),
+ "Should reset all sync statuses to UNKNOWN after bookmarks sync ID mismatch"
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_history_resetSyncId() {
+ let syncId = await PlacesSyncUtils.history.getSyncId();
+ strictEqual(syncId, "", "Should start with empty history sync ID");
+
+ info("Assign new history sync ID for first time");
+ let newSyncId = await PlacesSyncUtils.history.resetSyncId();
+ syncId = await PlacesSyncUtils.history.getSyncId();
+ equal(newSyncId, syncId, "Should assign new history sync ID for first time");
+
+ info("Set history last sync time");
+ let lastSync = Date.now() / 1000;
+ await PlacesSyncUtils.history.setLastSync(lastSync);
+ equal(
+ await PlacesSyncUtils.history.getLastSync(),
+ lastSync,
+ "Should record history last sync time"
+ );
+
+ newSyncId = await PlacesSyncUtils.history.resetSyncId();
+ notEqual(
+ newSyncId,
+ syncId,
+ "Should set new history sync ID if one already exists"
+ );
+ strictEqual(
+ await PlacesSyncUtils.history.getLastSync(),
+ 0,
+ "Should reset history last sync time after resetting sync ID"
+ );
+
+ await PlacesSyncUtils.history.reset();
+});
+
+add_task(async function test_history_ensureCurrentSyncId() {
+ info("Assign new history sync ID");
+ await PlacesSyncUtils.history.ensureCurrentSyncId("syncIdAAAAAA");
+ equal(
+ await PlacesSyncUtils.history.getSyncId(),
+ "syncIdAAAAAA",
+ "Should assign history sync ID if one doesn't exist"
+ );
+
+ info("Ensure existing history sync ID matches");
+ let lastSync = Date.now() / 1000;
+ await PlacesSyncUtils.history.setLastSync(lastSync);
+ await PlacesSyncUtils.history.ensureCurrentSyncId("syncIdAAAAAA");
+
+ equal(
+ await PlacesSyncUtils.history.getSyncId(),
+ "syncIdAAAAAA",
+ "Should keep existing history sync ID on match"
+ );
+ equal(
+ await PlacesSyncUtils.history.getLastSync(),
+ lastSync,
+ "Should keep existing history last sync time on sync ID match"
+ );
+
+ info("Replace existing history sync ID with new ID");
+ await PlacesSyncUtils.history.ensureCurrentSyncId("syncIdBBBBBB");
+
+ equal(
+ await PlacesSyncUtils.history.getSyncId(),
+ "syncIdBBBBBB",
+ "Should replace existing history sync ID on mismatch"
+ );
+ strictEqual(
+ await PlacesSyncUtils.history.getLastSync(),
+ 0,
+ "Should reset history last sync time on sync ID mismatch"
+ );
+
+ await PlacesSyncUtils.history.reset();
+});