--- a/toolkit/components/places/tests/sync/head_sync.js
+++ b/toolkit/components/places/tests/sync/head_sync.js
@@ -49,16 +49,35 @@ function inspectChangeRecords(changeReco
for (let [id, record] of Object.entries(changeRecords)) {
(record.tombstone ? results.deleted : results.updated).push(id);
}
results.updated.sort();
results.deleted.sort();
return results;
}
+async function promiseManyDatesAdded(guids) {
+ let datesAdded = new Map();
+ let db = await PlacesUtils.promiseDBConnection();
+ for (let chunk of PlacesSyncUtils.chunkArray(guids, 100)) {
+ let rows = await db.executeCached(`
+ SELECT guid, dateAdded FROM moz_bookmarks
+ WHERE guid IN (${new Array(chunk.length).fill("?").join(",")})`,
+ chunk);
+ if (rows.length != chunk.length) {
+ throw new TypeError("Can't fetch date added for nonexistent items");
+ }
+ for (let row of rows) {
+ let dateAdded = row.getResultByName("dateAdded") / 1000;
+ datesAdded.set(row.getResultByName("guid"), dateAdded);
+ }
+ }
+ return datesAdded;
+}
+
async function assertLocalTree(rootGuid, expected, message) {
function bookmarkNodeToInfo(node) {
let { guid, index, title, typeCode: type } = node;
let itemInfo = { guid, index, title, type };
if (node.annos) {
let syncableAnnos = node.annos.filter(anno => [
PlacesSyncUtils.bookmarks.DESCRIPTION_ANNO,
PlacesSyncUtils.bookmarks.SIDEBAR_ANNO,
@@ -85,16 +104,79 @@ async function assertLocalTree(rootGuid,
let actual = bookmarkNodeToInfo(root);
if (!ObjectUtils.deepEqual(actual, expected)) {
info(`Expected structure for ${rootGuid}: ${JSON.stringify(expected)}`);
info(`Actual structure for ${rootGuid}: ${JSON.stringify(actual)}`);
throw new Assert.constructor.AssertionError({ actual, expected, message });
}
}
+// Writes a changeset back to the mirror, simulating a successful upload.
+async function writeUploadedChanges(buf, changesToUpload) {
+ let records = Object.keys(changesToUpload).map(
+ recordId => changesToUpload[recordId].cleartext
+ );
+ await buf.store(records, { needsMerge: false });
+}
+
+async function assertRemoteTree(buf, expected, message) {
+ let itemInfos = new Map();
+
+ let itemRows = await buf.db.execute(`
+ SELECT v.guid, v.kind, v.needsMerge, IFNULL(s.position, -1) AS position,
+ v.title, u.url, v.keyword, v.tagFolderName, v.description,
+ v.loadInSidebar, v.smartBookmarkName, v.feedURL, v.siteURL
+ FROM items v
+ LEFT JOIN urls u ON u.id = v.urlId
+ LEFT JOIN structure s ON s.guid = v.guid
+ WHERE NOT v.isDeleted`);
+
+ for (let row of itemRows) {
+ let guid = row.getResultByName("guid");
+ let itemInfo = {
+ guid,
+ needsMerge: !!row.getResultByName("needsMerge"),
+ };
+ for (let name of ["kind", "position", "title", "url", "keyword",
+ "tagFolderName", "description", "loadInSidebar",
+ "smartBookmarkName", "feedURL", "siteURL"]) {
+ let value = row.getResultByName(name);
+ if (value !== null) {
+ itemInfo[name] = value;
+ }
+ }
+ itemInfos.set(guid, itemInfo);
+ }
+
+ function mirrorNodeToInfo(node) {
+ if (!itemInfos.has(node.guid)) {
+ throw new TypeError(`Missing item info for ${node.guid}`);
+ }
+ let itemInfo = itemInfos.get(node.guid);
+ if (node.children.length) {
+ itemInfo.children = node.children.map(mirrorNodeToInfo);
+ }
+ return itemInfo;
+ }
+
+ let remoteTree = await buf.fetchRemoteTree();
+ let actual = { root: mirrorNodeToInfo(remoteTree.root) };
+
+ let deleted = Array.from(remoteTree.deletedGuids).sort();
+ if (deleted.length) {
+ actual.deleted = deleted;
+ }
+
+ if (!ObjectUtils.deepEqual(actual, expected)) {
+ info(`Expected remote tree: ${JSON.stringify(expected)}`);
+ info(`Actual remote tree: ${JSON.stringify(actual)}`);
+ throw new Assert.constructor.AssertionError({ actual, expected, message });
+ }
+}
+
function makeLivemarkServer() {
let server = new HttpServer();
server.registerPrefixHandler("/feed/", do_get_file("./livemark.xml"));
server.start(-1);
return {
server,
get site() {
let { identity } = server;
--- a/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
@@ -93,20 +93,64 @@ add_task(async function test_value_struc
info("Apply remote");
let observer = expectBookmarkChangeNotifications();
let changesToUpload = await buf.apply({
remoteTimeSeconds: Date.now() / 1000,
});
deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
- let idsToUpload = inspectChangeRecords(changesToUpload);
- deepEqual(idsToUpload, {
- updated: ["bookmarkBBBB", "folderAAAAAA", "folderDDDDDD"],
- deleted: [],
+ let datesAdded = await promiseManyDatesAdded(["bookmarkBBBB", "folderAAAAAA",
+ "folderDDDDDD"]);
+ deepEqual(changesToUpload, {
+ bookmarkBBBB: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkBBBB",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ parentid: "folderDDDDDD",
+ hasDupe: true,
+ parentName: "D (remote)",
+ dateAdded: datesAdded.get("bookmarkBBBB"),
+ },
+ },
+ folderAAAAAA: {
+ tombstone: false,
+ counter: 3,
+ synced: false,
+ cleartext: {
+ id: "folderAAAAAA",
+ type: "folder",
+ title: "A (local)",
+ children: ["bookmarkCCCC"],
+ parentid: "menu",
+ hasDupe: true,
+ parentName: "menu",
+ dateAdded: datesAdded.get("folderAAAAAA"),
+ },
+ },
+ folderDDDDDD: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderDDDDDD",
+ type: "folder",
+ title: "D (remote)",
+ children: ["bookmarkEEEE", "bookmarkBBBB"],
+ parentid: "menu",
+ hasDupe: true,
+ parentName: "menu",
+ dateAdded: datesAdded.get("folderDDDDDD"),
+ },
+ },
}, "Should upload records for merged and new local items");
let localItemIds = await PlacesUtils.promiseManyItemIds(["folderAAAAAA",
"bookmarkEEEE", "bookmarkBBBB", "folderDDDDDD"]);
observer.check([{
name: "onItemMoved",
params: { itemId: localItemIds.get("bookmarkEEEE"),
oldParentId: localItemIds.get("folderDDDDDD"),
@@ -167,17 +211,84 @@ add_task(async function test_value_struc
}, {
guid: "bookmarkBBBB",
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
index: 1,
title: "B",
url: "http://example.com/b",
}],
}],
- }, "Should reconcile structure and value changes");
+ }, "Should reconcile structure and value changes locally");
+
+ await writeUploadedChanges(buf, changesToUpload);
+ await assertRemoteTree(buf, {
+ root: {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ kind: SyncedBookmarksMirror.KIND.FOLDER,
+ needsMerge: false,
+ position: -1,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ kind: SyncedBookmarksMirror.KIND.FOLDER,
+ needsMerge: false,
+ position: 0,
+ children: [{
+ guid: "folderAAAAAA",
+ kind: SyncedBookmarksMirror.KIND.FOLDER,
+ needsMerge: false,
+ position: 0,
+ title: "A (local)",
+ children: [{
+ guid: "bookmarkCCCC",
+ kind: SyncedBookmarksMirror.KIND.BOOKMARK,
+ needsMerge: false,
+ position: 0,
+ title: "C",
+ url: "http://example.com/c",
+ }],
+ }, {
+ guid: "folderDDDDDD",
+ kind: SyncedBookmarksMirror.KIND.FOLDER,
+ position: 1,
+ needsMerge: false,
+ title: "D (remote)",
+ children: [{
+ guid: "bookmarkEEEE",
+ kind: SyncedBookmarksMirror.KIND.BOOKMARK,
+ needsMerge: false,
+ position: 0,
+ title: "E",
+ url: "http://example.com/e",
+ }, {
+ guid: "bookmarkBBBB",
+ kind: SyncedBookmarksMirror.KIND.BOOKMARK,
+ needsMerge: false,
+ position: 1,
+ title: "B",
+ url: "http://example.com/b",
+ }],
+ }],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ kind: SyncedBookmarksMirror.KIND.FOLDER,
+ needsMerge: false,
+ position: 1,
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ kind: SyncedBookmarksMirror.KIND.FOLDER,
+ needsMerge: false,
+ position: 2,
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ kind: SyncedBookmarksMirror.KIND.FOLDER,
+ needsMerge: false,
+ position: 3,
+ }],
+ },
+ }, "Should update mirror with reconciled structure and value changes");
await buf.finalize();
await PlacesUtils.bookmarks.eraseEverything();
await PlacesSyncUtils.bookmarks.reset();
});
add_task(async function test_move() {
let buf = await openMirror("move");