michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: Cu.import("resource://gre/modules/PlacesUtils.jsm"); michael@0: Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm"); michael@0: Cu.import("resource://services-common/async.js"); michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-sync/engines.js"); michael@0: Cu.import("resource://services-sync/engines/bookmarks.js"); michael@0: Cu.import("resource://services-sync/service.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://testing-common/services/sync/utils.js"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: Service.engineManager.register(BookmarksEngine); michael@0: michael@0: add_test(function bad_record_allIDs() { michael@0: let server = new SyncServer(); michael@0: server.start(); michael@0: let syncTesting = new SyncTestingInfrastructure(server.server); michael@0: michael@0: _("Ensure that bad Places queries don't cause an error in getAllIDs."); michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: let badRecordID = PlacesUtils.bookmarks.insertBookmark( michael@0: PlacesUtils.bookmarks.toolbarFolder, michael@0: Utils.makeURI("place:folder=1138"), michael@0: PlacesUtils.bookmarks.DEFAULT_INDEX, michael@0: null); michael@0: michael@0: do_check_true(badRecordID > 0); michael@0: _("Record is " + badRecordID); michael@0: _("Type: " + PlacesUtils.bookmarks.getItemType(badRecordID)); michael@0: michael@0: _("Fetching children."); michael@0: store._getChildren("toolbar", {}); michael@0: michael@0: _("Fetching all IDs."); michael@0: let all = store.getAllIDs(); michael@0: michael@0: _("All IDs: " + JSON.stringify(all)); michael@0: do_check_true("menu" in all); michael@0: do_check_true("toolbar" in all); michael@0: michael@0: _("Clean up."); michael@0: PlacesUtils.bookmarks.removeItem(badRecordID); michael@0: server.stop(run_next_test); michael@0: }); michael@0: michael@0: add_test(function test_ID_caching() { michael@0: let server = new SyncServer(); michael@0: server.start(); michael@0: let syncTesting = new SyncTestingInfrastructure(server.server); michael@0: michael@0: _("Ensure that Places IDs are not cached."); michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: _("All IDs: " + JSON.stringify(store.getAllIDs())); michael@0: michael@0: let mobileID = store.idForGUID("mobile"); michael@0: _("Change the GUID for that item, and drop the mobile anno."); michael@0: store._setGUID(mobileID, "abcdefghijkl"); michael@0: PlacesUtils.annotations.removeItemAnnotation(mobileID, "mobile/bookmarksRoot"); michael@0: michael@0: let err; michael@0: let newMobileID; michael@0: michael@0: // With noCreate, we don't find an entry. michael@0: try { michael@0: newMobileID = store.idForGUID("mobile", true); michael@0: _("New mobile ID: " + newMobileID); michael@0: } catch (ex) { michael@0: err = ex; michael@0: _("Error: " + Utils.exceptionStr(err)); michael@0: } michael@0: michael@0: do_check_true(!err); michael@0: michael@0: // With !noCreate, lookup works, and it's different. michael@0: newMobileID = store.idForGUID("mobile", false); michael@0: _("New mobile ID: " + newMobileID); michael@0: do_check_true(!!newMobileID); michael@0: do_check_neq(newMobileID, mobileID); michael@0: michael@0: // And it's repeatable, even with creation enabled. michael@0: do_check_eq(newMobileID, store.idForGUID("mobile", false)); michael@0: michael@0: do_check_eq(store.GUIDForId(mobileID), "abcdefghijkl"); michael@0: server.stop(run_next_test); michael@0: }); michael@0: michael@0: function serverForFoo(engine) { michael@0: return serverForUsers({"foo": "password"}, { michael@0: meta: {global: {engines: {bookmarks: {version: engine.version, michael@0: syncID: engine.syncID}}}}, michael@0: bookmarks: {} michael@0: }); michael@0: } michael@0: michael@0: add_test(function test_processIncoming_error_orderChildren() { michael@0: _("Ensure that _orderChildren() is called even when _processIncoming() throws an error."); michael@0: michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: let server = serverForFoo(engine); michael@0: new SyncTestingInfrastructure(server.server); michael@0: michael@0: let collection = server.user("foo").collection("bookmarks"); michael@0: michael@0: try { michael@0: michael@0: let folder1_id = PlacesUtils.bookmarks.createFolder( michael@0: PlacesUtils.bookmarks.toolbarFolder, "Folder 1", 0); michael@0: let folder1_guid = store.GUIDForId(folder1_id); michael@0: michael@0: let fxuri = Utils.makeURI("http://getfirefox.com/"); michael@0: let tburi = Utils.makeURI("http://getthunderbird.com/"); michael@0: michael@0: let bmk1_id = PlacesUtils.bookmarks.insertBookmark( michael@0: folder1_id, fxuri, PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!"); michael@0: let bmk1_guid = store.GUIDForId(bmk1_id); michael@0: let bmk2_id = PlacesUtils.bookmarks.insertBookmark( michael@0: folder1_id, tburi, PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Thunderbird!"); michael@0: let bmk2_guid = store.GUIDForId(bmk2_id); michael@0: michael@0: // Create a server record for folder1 where we flip the order of michael@0: // the children. michael@0: let folder1_payload = store.createRecord(folder1_guid).cleartext; michael@0: folder1_payload.children.reverse(); michael@0: collection.insert(folder1_guid, encryptPayload(folder1_payload)); michael@0: michael@0: // Create a bogus record that when synced down will provoke a michael@0: // network error which in turn provokes an exception in _processIncoming. michael@0: const BOGUS_GUID = "zzzzzzzzzzzz"; michael@0: let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!"); michael@0: bogus_record.get = function get() { michael@0: throw "Sync this!"; michael@0: }; michael@0: michael@0: // Make the 10 minutes old so it will only be synced in the toFetch phase. michael@0: bogus_record.modified = Date.now() / 1000 - 60 * 10; michael@0: engine.lastSync = Date.now() / 1000 - 60; michael@0: engine.toFetch = [BOGUS_GUID]; michael@0: michael@0: let error; michael@0: try { michael@0: engine.sync(); michael@0: } catch(ex) { michael@0: error = ex; michael@0: } michael@0: do_check_true(!!error); michael@0: michael@0: // Verify that the bookmark order has been applied. michael@0: let new_children = store.createRecord(folder1_guid).children; michael@0: do_check_eq(new_children.length, 2); michael@0: do_check_eq(new_children[0], folder1_payload.children[0]); michael@0: do_check_eq(new_children[1], folder1_payload.children[1]); michael@0: michael@0: do_check_eq(PlacesUtils.bookmarks.getItemIndex(bmk1_id), 1); michael@0: do_check_eq(PlacesUtils.bookmarks.getItemIndex(bmk2_id), 0); michael@0: michael@0: } finally { michael@0: store.wipe(); michael@0: Svc.Prefs.resetBranch(""); michael@0: Service.recordManager.clearCache(); michael@0: server.stop(run_next_test); michael@0: } michael@0: }); michael@0: michael@0: add_task(function test_restorePromptsReupload() { michael@0: _("Ensure that restoring from a backup will reupload all records."); michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: let server = serverForFoo(engine); michael@0: new SyncTestingInfrastructure(server.server); michael@0: michael@0: let collection = server.user("foo").collection("bookmarks"); michael@0: michael@0: Svc.Obs.notify("weave:engine:start-tracking"); // We skip usual startup... michael@0: michael@0: try { michael@0: michael@0: let folder1_id = PlacesUtils.bookmarks.createFolder( michael@0: PlacesUtils.bookmarks.toolbarFolder, "Folder 1", 0); michael@0: let folder1_guid = store.GUIDForId(folder1_id); michael@0: _("Folder 1: " + folder1_id + ", " + folder1_guid); michael@0: michael@0: let fxuri = Utils.makeURI("http://getfirefox.com/"); michael@0: let tburi = Utils.makeURI("http://getthunderbird.com/"); michael@0: michael@0: _("Create a single record."); michael@0: let bmk1_id = PlacesUtils.bookmarks.insertBookmark( michael@0: folder1_id, fxuri, PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!"); michael@0: let bmk1_guid = store.GUIDForId(bmk1_id); michael@0: _("Get Firefox!: " + bmk1_id + ", " + bmk1_guid); michael@0: michael@0: michael@0: let dirSvc = Cc["@mozilla.org/file/directory_service;1"] michael@0: .getService(Ci.nsIProperties); michael@0: michael@0: let backupFile = dirSvc.get("TmpD", Ci.nsILocalFile); michael@0: michael@0: _("Make a backup."); michael@0: backupFile.append("t_b_e_" + Date.now() + ".json"); michael@0: michael@0: _("Backing up to file " + backupFile.path); michael@0: backupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600); michael@0: yield BookmarkJSONUtils.exportToFile(backupFile); michael@0: michael@0: _("Create a different record and sync."); michael@0: let bmk2_id = PlacesUtils.bookmarks.insertBookmark( michael@0: folder1_id, tburi, PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Thunderbird!"); michael@0: let bmk2_guid = store.GUIDForId(bmk2_id); michael@0: _("Get Thunderbird!: " + bmk2_id + ", " + bmk2_guid); michael@0: michael@0: PlacesUtils.bookmarks.removeItem(bmk1_id); michael@0: michael@0: let error; michael@0: try { michael@0: engine.sync(); michael@0: } catch(ex) { michael@0: error = ex; michael@0: _("Got error: " + Utils.exceptionStr(ex)); michael@0: } michael@0: do_check_true(!error); michael@0: michael@0: _("Verify that there's only one bookmark on the server, and it's Thunderbird."); michael@0: // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu... michael@0: let wbos = collection.keys(function (id) { michael@0: return ["menu", "toolbar", "mobile", folder1_guid].indexOf(id) == -1; michael@0: }); michael@0: do_check_eq(wbos.length, 1); michael@0: do_check_eq(wbos[0], bmk2_guid); michael@0: michael@0: _("Now restore from a backup."); michael@0: yield BookmarkJSONUtils.importFromFile(backupFile, true); michael@0: michael@0: _("Ensure we have the bookmarks we expect locally."); michael@0: let guids = store.getAllIDs(); michael@0: _("GUIDs: " + JSON.stringify(guids)); michael@0: let found = false; michael@0: let count = 0; michael@0: let newFX; michael@0: for (let guid in guids) { michael@0: count++; michael@0: let id = store.idForGUID(guid, true); michael@0: // Only one bookmark, so _all_ should be Firefox! michael@0: if (PlacesUtils.bookmarks.getItemType(id) == PlacesUtils.bookmarks.TYPE_BOOKMARK) { michael@0: let uri = PlacesUtils.bookmarks.getBookmarkURI(id); michael@0: _("Found URI " + uri.spec + " for GUID " + guid); michael@0: do_check_eq(uri.spec, fxuri.spec); michael@0: newFX = guid; // Save the new GUID after restore. michael@0: found = true; // Only runs if the above check passes. michael@0: } michael@0: } michael@0: _("We found it: " + found); michael@0: do_check_true(found); michael@0: michael@0: _("Have the correct number of IDs locally, too."); michael@0: do_check_eq(count, ["menu", "toolbar", folder1_id, bmk1_id].length); michael@0: michael@0: _("Sync again. This'll wipe bookmarks from the server."); michael@0: try { michael@0: engine.sync(); michael@0: } catch(ex) { michael@0: error = ex; michael@0: _("Got error: " + Utils.exceptionStr(ex)); michael@0: } michael@0: do_check_true(!error); michael@0: michael@0: _("Verify that there's only one bookmark on the server, and it's Firefox."); michael@0: // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu... michael@0: let payloads = server.user("foo").collection("bookmarks").payloads(); michael@0: let bookmarkWBOs = payloads.filter(function (wbo) { michael@0: return wbo.type == "bookmark"; michael@0: }); michael@0: let folderWBOs = payloads.filter(function (wbo) { michael@0: return ((wbo.type == "folder") && michael@0: (wbo.id != "menu") && michael@0: (wbo.id != "toolbar")); michael@0: }); michael@0: michael@0: do_check_eq(bookmarkWBOs.length, 1); michael@0: do_check_eq(bookmarkWBOs[0].id, newFX); michael@0: do_check_eq(bookmarkWBOs[0].bmkUri, fxuri.spec); michael@0: do_check_eq(bookmarkWBOs[0].title, "Get Firefox!"); michael@0: michael@0: _("Our old friend Folder 1 is still in play."); michael@0: do_check_eq(folderWBOs.length, 1); michael@0: do_check_eq(folderWBOs[0].title, "Folder 1"); michael@0: michael@0: } finally { michael@0: store.wipe(); michael@0: Svc.Prefs.resetBranch(""); michael@0: Service.recordManager.clearCache(); michael@0: let deferred = Promise.defer(); michael@0: server.stop(deferred.resolve); michael@0: yield deferred.promise; michael@0: } michael@0: }); michael@0: michael@0: function FakeRecord(constructor, r) { michael@0: constructor.call(this, "bookmarks", r.id); michael@0: for (let x in r) { michael@0: this[x] = r[x]; michael@0: } michael@0: } michael@0: michael@0: // Bug 632287. michael@0: add_test(function test_mismatched_types() { michael@0: _("Ensure that handling a record that changes type causes deletion " + michael@0: "then re-adding."); michael@0: michael@0: let oldRecord = { michael@0: "id": "l1nZZXfB8nC7", michael@0: "type":"folder", michael@0: "parentName":"Bookmarks Toolbar", michael@0: "title":"Innerst i Sneglehode", michael@0: "description":null, michael@0: "parentid": "toolbar" michael@0: }; michael@0: michael@0: let newRecord = { michael@0: "id": "l1nZZXfB8nC7", michael@0: "type":"livemark", michael@0: "siteUri":"http://sneglehode.wordpress.com/", michael@0: "feedUri":"http://sneglehode.wordpress.com/feed/", michael@0: "parentName":"Bookmarks Toolbar", michael@0: "title":"Innerst i Sneglehode", michael@0: "description":null, michael@0: "children": michael@0: ["HCRq40Rnxhrd", "YeyWCV1RVsYw", "GCceVZMhvMbP", "sYi2hevdArlF", michael@0: "vjbZlPlSyGY8", "UtjUhVyrpeG6", "rVq8WMG2wfZI", "Lx0tcy43ZKhZ", michael@0: "oT74WwV8_j4P", "IztsItWVSo3-"], michael@0: "parentid": "toolbar" michael@0: }; michael@0: michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: let server = serverForFoo(engine); michael@0: new SyncTestingInfrastructure(server.server); michael@0: michael@0: _("GUID: " + store.GUIDForId(6, true)); michael@0: michael@0: try { michael@0: let bms = PlacesUtils.bookmarks; michael@0: let oldR = new FakeRecord(BookmarkFolder, oldRecord); michael@0: let newR = new FakeRecord(Livemark, newRecord); michael@0: oldR._parent = PlacesUtils.bookmarks.toolbarFolder; michael@0: newR._parent = PlacesUtils.bookmarks.toolbarFolder; michael@0: michael@0: store.applyIncoming(oldR); michael@0: _("Applied old. It's a folder."); michael@0: let oldID = store.idForGUID(oldR.id); michael@0: _("Old ID: " + oldID); michael@0: do_check_eq(bms.getItemType(oldID), bms.TYPE_FOLDER); michael@0: do_check_false(PlacesUtils.annotations michael@0: .itemHasAnnotation(oldID, PlacesUtils.LMANNO_FEEDURI)); michael@0: michael@0: store.applyIncoming(newR); michael@0: let newID = store.idForGUID(newR.id); michael@0: _("New ID: " + newID); michael@0: michael@0: _("Applied new. It's a livemark."); michael@0: do_check_eq(bms.getItemType(newID), bms.TYPE_FOLDER); michael@0: do_check_true(PlacesUtils.annotations michael@0: .itemHasAnnotation(newID, PlacesUtils.LMANNO_FEEDURI)); michael@0: michael@0: } finally { michael@0: store.wipe(); michael@0: Svc.Prefs.resetBranch(""); michael@0: Service.recordManager.clearCache(); michael@0: server.stop(run_next_test); michael@0: } michael@0: }); michael@0: michael@0: add_test(function test_bookmark_guidMap_fail() { michael@0: _("Ensure that failures building the GUID map cause early death."); michael@0: michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: michael@0: let store = engine._store; michael@0: let server = serverForFoo(engine); michael@0: let coll = server.user("foo").collection("bookmarks"); michael@0: new SyncTestingInfrastructure(server.server); michael@0: michael@0: // Add one item to the server. michael@0: let itemID = PlacesUtils.bookmarks.createFolder( michael@0: PlacesUtils.bookmarks.toolbarFolder, "Folder 1", 0); michael@0: let itemGUID = store.GUIDForId(itemID); michael@0: let itemPayload = store.createRecord(itemGUID).cleartext; michael@0: coll.insert(itemGUID, encryptPayload(itemPayload)); michael@0: michael@0: engine.lastSync = 1; // So we don't back up. michael@0: michael@0: // Make building the GUID map fail. michael@0: store.getAllIDs = function () { throw "Nooo"; }; michael@0: michael@0: // Ensure that we throw when accessing _guidMap. michael@0: engine._syncStartup(); michael@0: _("No error."); michael@0: do_check_false(engine._guidMapFailed); michael@0: michael@0: _("We get an error if building _guidMap fails in use."); michael@0: let err; michael@0: try { michael@0: _(engine._guidMap); michael@0: } catch (ex) { michael@0: err = ex; michael@0: } michael@0: do_check_eq(err.code, Engine.prototype.eEngineAbortApplyIncoming); michael@0: do_check_eq(err.cause, "Nooo"); michael@0: michael@0: _("We get an error and abort during processIncoming."); michael@0: err = undefined; michael@0: try { michael@0: engine._processIncoming(); michael@0: } catch (ex) { michael@0: err = ex; michael@0: } michael@0: do_check_eq(err, "Nooo"); michael@0: michael@0: server.stop(run_next_test); michael@0: }); michael@0: michael@0: add_test(function test_bookmark_is_taggable() { michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: michael@0: do_check_true(store.isTaggable("bookmark")); michael@0: do_check_true(store.isTaggable("microsummary")); michael@0: do_check_true(store.isTaggable("query")); michael@0: do_check_false(store.isTaggable("folder")); michael@0: do_check_false(store.isTaggable("livemark")); michael@0: do_check_false(store.isTaggable(null)); michael@0: do_check_false(store.isTaggable(undefined)); michael@0: do_check_false(store.isTaggable("")); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_bookmark_tag_but_no_uri() { michael@0: _("Ensure that a bookmark record with tags, but no URI, doesn't throw an exception."); michael@0: michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: michael@0: // We're simply checking that no exception is thrown, so michael@0: // no actual checks in this test. michael@0: michael@0: store._tagURI(null, ["foo"]); michael@0: store._tagURI(null, null); michael@0: store._tagURI(Utils.makeURI("about:fake"), null); michael@0: michael@0: let record = { michael@0: _parent: PlacesUtils.bookmarks.toolbarFolder, michael@0: id: Utils.makeGUID(), michael@0: description: "", michael@0: tags: ["foo"], michael@0: title: "Taggy tag", michael@0: type: "folder" michael@0: }; michael@0: michael@0: // Because update() walks the cleartext. michael@0: record.cleartext = record; michael@0: michael@0: store.create(record); michael@0: record.tags = ["bar"]; michael@0: store.update(record); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_misreconciled_root() { michael@0: _("Ensure that we don't reconcile an arbitrary record with a root."); michael@0: michael@0: let engine = new BookmarksEngine(Service); michael@0: let store = engine._store; michael@0: let server = serverForFoo(engine); michael@0: michael@0: // Log real hard for this test. michael@0: store._log.trace = store._log.debug; michael@0: engine._log.trace = engine._log.debug; michael@0: michael@0: engine._syncStartup(); michael@0: michael@0: // Let's find out where the toolbar is right now. michael@0: let toolbarBefore = store.createRecord("toolbar", "bookmarks"); michael@0: let toolbarIDBefore = store.idForGUID("toolbar"); michael@0: do_check_neq(-1, toolbarIDBefore); michael@0: michael@0: let parentGUIDBefore = toolbarBefore.parentid; michael@0: let parentIDBefore = store.idForGUID(parentGUIDBefore); michael@0: do_check_neq(-1, parentIDBefore); michael@0: do_check_eq("string", typeof(parentGUIDBefore)); michael@0: michael@0: _("Current parent: " + parentGUIDBefore + " (" + parentIDBefore + ")."); michael@0: michael@0: let to_apply = { michael@0: id: "zzzzzzzzzzzz", michael@0: type: "folder", michael@0: title: "Bookmarks Toolbar", michael@0: description: "Now you're for it.", michael@0: parentName: "", michael@0: parentid: "mobile", // Why not? michael@0: children: [], michael@0: }; michael@0: michael@0: let rec = new FakeRecord(BookmarkFolder, to_apply); michael@0: let encrypted = encryptPayload(rec.cleartext); michael@0: encrypted.decrypt = function () { michael@0: for (let x in rec) { michael@0: encrypted[x] = rec[x]; michael@0: } michael@0: }; michael@0: michael@0: _("Applying record."); michael@0: engine._processIncoming({ michael@0: get: function () { michael@0: this.recordHandler(encrypted); michael@0: return {success: true} michael@0: }, michael@0: }); michael@0: michael@0: // Ensure that afterwards, toolbar is still there. michael@0: // As of 2012-12-05, this only passes because Places doesn't use "toolbar" as michael@0: // the real GUID, instead using a generated one. Sync does the translation. michael@0: let toolbarAfter = store.createRecord("toolbar", "bookmarks"); michael@0: let parentGUIDAfter = toolbarAfter.parentid; michael@0: let parentIDAfter = store.idForGUID(parentGUIDAfter); michael@0: do_check_eq(store.GUIDForId(toolbarIDBefore), "toolbar"); michael@0: do_check_eq(parentGUIDBefore, parentGUIDAfter); michael@0: do_check_eq(parentIDBefore, parentIDAfter); michael@0: michael@0: server.stop(run_next_test); michael@0: }); michael@0: michael@0: function run_test() { michael@0: initTestLogging("Trace"); michael@0: generateNewKeys(Service.collectionKeys); michael@0: run_next_test(); michael@0: }