/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const { BookmarkValidator } = ChromeUtils.import( "resource://services-sync/bookmark_validator.js" ); function run_test() { do_get_profile(); run_next_test(); } async function inspectServerRecords(data) { let validator = new BookmarkValidator(); return validator.inspectServerRecords(data); } async function compareServerWithClient(server, client) { let validator = new BookmarkValidator(); return validator.compareServerWithClient(server, client); } add_task(async function test_isr_rootOnServer() { let c = await inspectServerRecords([ { id: "places", type: "folder", children: [], }, ]); ok(c.problemData.rootOnServer); }); add_task(async function test_isr_empty() { let c = await inspectServerRecords([]); ok(!c.problemData.rootOnServer); notEqual(c.root, null); }); add_task(async function test_isr_cycles() { let c = ( await inspectServerRecords([ { id: "C", type: "folder", children: ["A", "B"], parentid: "places" }, { id: "A", type: "folder", children: ["B"], parentid: "B" }, { id: "B", type: "folder", children: ["A"], parentid: "A" }, ]) ).problemData; equal(c.cycles.length, 1); ok(c.cycles[0].includes("A")); ok(c.cycles[0].includes("B")); }); add_task(async function test_isr_orphansMultiParents() { let c = ( await inspectServerRecords([ { id: "A", type: "bookmark", parentid: "D" }, { id: "B", type: "folder", parentid: "places", children: ["A"] }, { id: "C", type: "folder", parentid: "places", children: ["A"] }, ]) ).problemData; deepEqual(c.orphans, [{ id: "A", parent: "D" }]); equal(c.multipleParents.length, 1); ok(c.multipleParents[0].parents.includes("B")); ok(c.multipleParents[0].parents.includes("C")); }); add_task(async function test_isr_orphansMultiParents2() { let c = ( await inspectServerRecords([ { id: "A", type: "bookmark", parentid: "D" }, { id: "B", type: "folder", parentid: "places", children: ["A"] }, ]) ).problemData; equal(c.orphans.length, 1); equal(c.orphans[0].id, "A"); equal(c.multipleParents.length, 0); }); add_task(async function test_isr_deletedParents() { let c = ( await inspectServerRecords([ { id: "A", type: "bookmark", parentid: "B" }, { id: "C", type: "folder", parentid: "places", children: ["A"] }, { id: "B", type: "item", deleted: true }, ]) ).problemData; deepEqual(c.deletedParents, ["A"]); }); add_task(async function test_isr_badChildren() { let c = ( await inspectServerRecords([ { id: "A", type: "bookmark", parentid: "places", children: ["B", "C"] }, { id: "C", type: "bookmark", parentid: "A" }, ]) ).problemData; deepEqual(c.childrenOnNonFolder, ["A"]); deepEqual(c.missingChildren, [{ parent: "A", child: "B" }]); deepEqual(c.parentNotFolder, ["C"]); }); add_task(async function test_isr_parentChildMismatches() { let c = ( await inspectServerRecords([ { id: "A", type: "folder", parentid: "places", children: [] }, { id: "B", type: "bookmark", parentid: "A" }, ]) ).problemData; deepEqual(c.parentChildMismatches, [{ parent: "A", child: "B" }]); }); add_task(async function test_isr_duplicatesAndMissingIDs() { let c = ( await inspectServerRecords([ { id: "A", type: "folder", parentid: "places", children: [] }, { id: "A", type: "folder", parentid: "places", children: [] }, { type: "folder", parentid: "places", children: [] }, ]) ).problemData; equal(c.missingIDs, 1); deepEqual(c.duplicates, ["A"]); }); add_task(async function test_isr_duplicateChildren() { let c = ( await inspectServerRecords([ { id: "A", type: "folder", parentid: "places", children: ["B", "B"] }, { id: "B", type: "bookmark", parentid: "A" }, ]) ).problemData; deepEqual(c.duplicateChildren, ["A"]); }); // Each compareServerWithClient test mutates these, so we can"t just keep them // global function getDummyServerAndClient() { let server = [ { id: "menu", parentid: "places", type: "folder", parentName: "", title: "foo", children: ["bbbbbbbbbbbb", "cccccccccccc"], }, { id: "bbbbbbbbbbbb", type: "bookmark", parentid: "menu", parentName: "foo", title: "bar", bmkUri: "http://baz.com", }, { id: "cccccccccccc", parentid: "menu", parentName: "foo", title: "", type: "query", bmkUri: "place:type=6&sort=14&maxResults=10", }, ]; let client = { guid: "root________", title: "", id: 1, type: "text/x-moz-place-container", children: [ { guid: "menu________", title: "foo", id: 1000, type: "text/x-moz-place-container", children: [ { guid: "bbbbbbbbbbbb", title: "bar", id: 1001, type: "text/x-moz-place", uri: "http://baz.com", }, { guid: "cccccccccccc", title: "", id: 1002, type: "text/x-moz-place", uri: "place:type=6&sort=14&maxResults=10", }, ], }, ], }; return { server, client }; } add_task(async function test_cswc_valid() { let { server, client } = getDummyServerAndClient(); let c = (await compareServerWithClient(server, client)).problemData; equal(c.clientMissing.length, 0); equal(c.serverMissing.length, 0); equal(c.differences.length, 0); }); add_task(async function test_cswc_serverMissing() { let { server, client } = getDummyServerAndClient(); // remove c server.pop(); server[0].children.pop(); let c = (await compareServerWithClient(server, client)).problemData; deepEqual(c.serverMissing, ["cccccccccccc"]); equal(c.clientMissing.length, 0); deepEqual(c.structuralDifferences, [ { id: "menu", differences: ["childGUIDs"] }, ]); }); add_task(async function test_cswc_clientMissing() { let { server, client } = getDummyServerAndClient(); client.children[0].children.pop(); let c = (await compareServerWithClient(server, client)).problemData; deepEqual(c.clientMissing, ["cccccccccccc"]); equal(c.serverMissing.length, 0); deepEqual(c.structuralDifferences, [ { id: "menu", differences: ["childGUIDs"] }, ]); }); add_task(async function test_cswc_differences() { { let { server, client } = getDummyServerAndClient(); client.children[0].children[0].title = "asdf"; let c = (await compareServerWithClient(server, client)).problemData; equal(c.clientMissing.length, 0); equal(c.serverMissing.length, 0); deepEqual(c.differences, [{ id: "bbbbbbbbbbbb", differences: ["title"] }]); } { let { server, client } = getDummyServerAndClient(); server[2].type = "bookmark"; let c = (await compareServerWithClient(server, client)).problemData; equal(c.clientMissing.length, 0); equal(c.serverMissing.length, 0); deepEqual(c.differences, [{ id: "cccccccccccc", differences: ["type"] }]); } }); add_task(async function test_cswc_differentURLs() { let { server, client } = getDummyServerAndClient(); client.children[0].children.push( { guid: "dddddddddddd", title: "Tag query", type: "text/x-moz-place", uri: "place:type=7&folder=80", }, { guid: "eeeeeeeeeeee", title: "Firefox", type: "text/x-moz-place", uri: "http://getfirefox.com", } ); server.push( { id: "dddddddddddd", parentid: "menu", parentName: "foo", title: "Tag query", type: "query", folderName: "taggy", bmkUri: "place:type=7&folder=90", }, { id: "eeeeeeeeeeee", parentid: "menu", parentName: "foo", title: "Firefox", type: "bookmark", bmkUri: "https://mozilla.org/firefox", } ); let c = (await compareServerWithClient(server, client)).problemData; equal(c.differences.length, 1); deepEqual(c.differences, [ { id: "eeeeeeeeeeee", differences: ["bmkUri"], }, ]); }); add_task(async function test_cswc_serverUnexpected() { let { server, client } = getDummyServerAndClient(); client.children.push({ guid: "dddddddddddd", title: "", id: 2000, type: "text/x-moz-place-container", children: [ { guid: "eeeeeeeeeeee", title: "History", type: "text/x-moz-place", uri: "place:type=3&sort=4", }, ], }); server.push( { id: "dddddddddddd", parentid: "places", parentName: "", title: "", type: "folder", children: ["eeeeeeeeeeee"], }, { id: "eeeeeeeeeeee", parentid: "dddddddddddd", parentName: "", title: "History", type: "query", bmkUri: "place:type=3&sort=4", } ); let c = (await compareServerWithClient(server, client)).problemData; equal(c.clientMissing.length, 0); equal(c.serverMissing.length, 0); equal(c.serverUnexpected.length, 2); deepEqual(c.serverUnexpected, ["dddddddddddd", "eeeeeeeeeeee"]); }); add_task(async function test_cswc_clientCycles() { await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.menuGuid, children: [ { // A query for the menu, referenced by its local ID instead of // `BOOKMARKS_MENU`. This should be reported as a cycle. guid: "dddddddddddd", url: `place:folder=${PlacesUtils.bookmarksMenuFolderId}`, title: "Bookmarks Menu", }, { // A query that references the menu, but excludes itself, so it can't // form a cycle. guid: "iiiiiiiiiiii", url: `place:folder=BOOKMARKS_MENU&folder=UNFILED_BOOKMARKS&` + `folder=TOOLBAR&queryType=1&sort=12&maxResults=10&` + `excludeQueries=1`, title: "Recently Bookmarked", }, ], }); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.toolbarGuid, children: [ { guid: "eeeeeeeeeeee", type: PlacesUtils.bookmarks.TYPE_FOLDER, children: [ { // A query for the toolbar in a subfolder. This should still be reported // as a cycle. guid: "ffffffffffff", url: "place:folder=TOOLBAR&sort=3", title: "Bookmarks Toolbar", }, ], }, ], }); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.unfiledGuid, children: [ { // A query for the menu. This shouldn't be reported as a cycle, since it // references a different root. guid: "gggggggggggg", url: "place:folder=BOOKMARKS_MENU&sort=5", title: "Bookmarks Menu", }, ], }); await PlacesUtils.bookmarks.insertTree({ guid: PlacesUtils.bookmarks.mobileGuid, children: [ { // A query referencing multiple roots, one of which forms a cycle by // referencing mobile. This is extremely unlikely, but it's cheap to // detect, so we still report it. guid: "hhhhhhhhhhhh", url: "place:folder=TOOLBAR&folder=MOBILE_BOOKMARKS&folder=UNFILED_BOOKMARKS&sort=1", title: "Toolbar, Mobile, Unfiled", }, ], }); let clientTree = await PlacesUtils.promiseBookmarksTree("", { includeItemIds: true, }); let c = (await compareServerWithClient([], clientTree)).problemData; deepEqual(c.clientCycles, [ ["menu", "dddddddddddd"], ["toolbar", "eeeeeeeeeeee", "ffffffffffff"], ["mobile", "hhhhhhhhhhhh"], ]); }); async function validationPing(server, client, duration) { let pingPromise = wait_for_ping(() => {}, true); // Allow "failing" pings, since having validation info indicates failure. // fake this entirely Svc.Obs.notify("weave:service:sync:start"); Svc.Obs.notify("weave:engine:sync:start", null, "bookmarks"); Svc.Obs.notify("weave:engine:sync:finish", null, "bookmarks"); let validator = new BookmarkValidator(); let { problemData } = await validator.compareServerWithClient(server, client); let data = { // We fake duration and version just so that we can verify they"re passed through. took: duration, version: validator.version, checked: server.length, problems: problemData.getSummary(true), }; Svc.Obs.notify("weave:engine:validate:finish", data, "bookmarks"); Svc.Obs.notify("weave:service:sync:finish"); return pingPromise; } add_task(async function test_telemetry_integration() { let { server, client } = getDummyServerAndClient(); // remove "c" server.pop(); server[0].children.pop(); const duration = 50; let ping = await validationPing(server, client, duration); ok(ping.engines); let bme = ping.engines.find(e => e.name === "bookmarks"); ok(bme); ok(bme.validation); ok(bme.validation.problems); equal(bme.validation.checked, server.length); equal(bme.validation.took, duration); bme.validation.problems.sort((a, b) => String(a.name).localeCompare(b.name)); equal(bme.validation.version, new BookmarkValidator().version); deepEqual(bme.validation.problems, [ { name: "badClientRoots", count: 3 }, { name: "sdiff:childGUIDs", count: 1 }, { name: "serverMissing", count: 1 }, { name: "structuralDifferences", count: 1 }, ]); });