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