--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -9,16 +9,17 @@ DIRS += [
'aushelper',
'followonsearch',
'formautofill',
'onboarding',
'pdfjs',
'pocket',
'screenshots',
'shield-recipe-client',
+ 'sync-repair',
'webcompat',
]
# Only include the following system add-ons if building Aurora or Nightly
if not CONFIG['RELEASE_OR_BETA']:
DIRS += [
'flyweb',
'presentation',
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/addon_validator.jsm
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://sync-repair/collection_validator.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+this.EXPORTED_SYMBOLS = ["AddonsValidator"];
+
+class AddonValidator extends CollectionValidator {
+ constructor() {
+ super("addons", "id", [
+ "addonID",
+ "enabled",
+ "applicationID",
+ "source"
+ ]);
+ }
+
+ async getClientItems() {
+ const installed = await AddonManager.getAllAddons();
+ const addonsWithPendingOperation = await AddonManager.getAddonsWithOperationsByTypes(["extension", "theme"]);
+ // Addons pending install won't be in the first list, but addons pending
+ // uninstall/enable/disable will be in both lists.
+ let all = new Map(installed.map(addon => [addon.id, addon]));
+ for (let addon of addonsWithPendingOperation) {
+ all.set(addon.id, addon);
+ }
+ // Convert to an array since Map.prototype.values returns an iterable
+ return [...all.values()];
+ }
+
+ normalizeClientItem(item) {
+ let enabled = !item.userDisabled;
+ if (item.pendingOperations & AddonManager.PENDING_ENABLE) {
+ enabled = true;
+ } else if (item.pendingOperations & AddonManager.PENDING_DISABLE) {
+ enabled = false;
+ }
+ return {
+ enabled,
+ id: item.syncGUID,
+ addonID: item.id,
+ applicationID: Services.appinfo.ID,
+ source: "amo", // check item.foreignInstall?
+ original: item
+ };
+ }
+
+ async normalizeServerItem(item) {
+ let guid = await this.engine._findDupe(item);
+ if (guid) {
+ item.id = guid;
+ }
+ return item;
+ }
+
+ clientUnderstands(item) {
+ return item.applicationID === Services.appinfo.ID;
+ }
+
+ syncedByClient(item) {
+ return !item.original.hidden &&
+ !item.original.isSystem &&
+ !(item.original.pendingOperations & AddonManager.PENDING_UNINSTALL) &&
+ this.engine.isAddonSyncable(item.original, true);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/bookmark_repair.jsm
@@ -0,0 +1,755 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = ["BookmarkRepairRequestor", "BookmarkRepairResponder"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/resource.js");
+Cu.import("resource://services-sync/telemetry.js");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/utils.js");
+
+Cu.import("resource://sync-repair/collection_repair.jsm");
+Cu.import("resource://sync-repair/doctor.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesSyncUtils",
+ "resource://gre/modules/PlacesSyncUtils.jsm");
+
+const log = Log.repository.getLogger("Sync.Engine.Bookmarks.Repair");
+
+const PREF_BRANCH = "services.sync.repairs.bookmarks.";
+
+// How long should we wait after sending a repair request before we give up?
+const RESPONSE_INTERVAL_TIMEOUT = 60 * 60 * 24 * 3; // 3 days
+
+// The maximum number of IDs we will request to be repaired. Beyond this
+// number we assume that trying to repair may do more harm than good and may
+// ask another client to wipe the server and reupload everything. Bug 1341972
+// is tracking that work.
+const MAX_REQUESTED_IDS = 1000;
+
+class AbortRepairError extends Error {
+ constructor(reason) {
+ super();
+ this.reason = reason;
+ }
+}
+
+// The states we can be in.
+const STATE = Object.freeze({
+ NOT_REPAIRING: "",
+
+ // We need to try to find another client to use.
+ NEED_NEW_CLIENT: "repair.need-new-client",
+
+ // We've sent the first request to a client.
+ SENT_REQUEST: "repair.sent",
+
+ // We've retried a request to a client.
+ SENT_SECOND_REQUEST: "repair.sent-again",
+
+ // There were no problems, but we've gone as far as we can.
+ FINISHED: "repair.finished",
+
+ // We've found an error that forces us to abort this entire repair cycle.
+ ABORTED: "repair.aborted",
+});
+
+// The preferences we use to hold our state.
+const PREF = Object.freeze({
+ // If a repair is in progress, this is the generated GUID for the "flow ID".
+ REPAIR_ID: "flowID",
+
+ // The IDs we are currently trying to obtain via the repair process, space sep'd.
+ REPAIR_MISSING_IDS: "ids",
+
+ // The ID of the client we're currently trying to get the missing items from.
+ REPAIR_CURRENT_CLIENT: "currentClient",
+
+ // The IDs of the clients we've previously tried to get the missing items
+ // from, space sep'd.
+ REPAIR_PREVIOUS_CLIENTS: "previousClients",
+
+ // The time, in seconds, when we initiated the most recent client request.
+ REPAIR_WHEN: "when",
+
+ // Our current state.
+ CURRENT_STATE: "state",
+});
+
+class BookmarkRepairRequestor extends CollectionRepairRequestor {
+ constructor(service = null) {
+ super(service);
+ this.prefs = new Preferences(PREF_BRANCH);
+ }
+
+ /* Check if any other clients connected to our account are current performing
+ a repair. A thin wrapper which exists mainly for mocking during tests.
+ */
+ anyClientsRepairing(flowID) {
+ return Doctor.anyClientsRepairing(this.service, "bookmarks", flowID);
+ }
+
+ /* Return a set of IDs we should request.
+ */
+ getProblemIDs(validationInfo) {
+ // Set of ids of "known bad records". Many of the validation issues will
+ // report duplicates -- if the server is missing a record, it is unlikely
+ // to cause only a single problem.
+ let ids = new Set();
+
+ // Note that we allow any of the validation problem fields to be missing so
+ // that tests have a slightly easier time, hence the `|| []` in each loop.
+
+ // Missing children records when the parent exists but a child doesn't.
+ for (let { parent, child } of validationInfo.problems.missingChildren || []) {
+ // We can't be sure if the child is missing or our copy of the parent is
+ // wrong, so request both
+ ids.add(parent);
+ ids.add(child);
+ }
+ if (ids.size > MAX_REQUESTED_IDS) {
+ return ids; // might as well give up here - we aren't going to repair.
+ }
+
+ // Orphans are when the child exists but the parent doesn't.
+ // This could either be a problem in the child (it's wrong about the node
+ // that should be its parent), or the parent could simply be missing.
+ for (let { parent, id } of validationInfo.problems.orphans || []) {
+ // Request both, to handle both cases
+ ids.add(id);
+ ids.add(parent);
+ }
+ if (ids.size > MAX_REQUESTED_IDS) {
+ return ids; // might as well give up here - we aren't going to repair.
+ }
+
+ // Entries where we have the parent but we have a record from the server that
+ // claims the child was deleted.
+ for (let { parent, child } of validationInfo.problems.deletedChildren || []) {
+ // Request both, since we don't know if it's a botched deletion or revival
+ ids.add(parent);
+ ids.add(child);
+ }
+ if (ids.size > MAX_REQUESTED_IDS) {
+ return ids; // might as well give up here - we aren't going to repair.
+ }
+
+ // Entries where the child references a parent that we don't have, but we
+ // have a record from the server that claims the parent was deleted.
+ for (let { parent, child } of validationInfo.problems.deletedParents || []) {
+ // Request both, since we don't know if it's a botched deletion or revival
+ ids.add(parent);
+ ids.add(child);
+ }
+ if (ids.size > MAX_REQUESTED_IDS) {
+ return ids; // might as well give up here - we aren't going to repair.
+ }
+
+ // Cases where the parent and child disagree about who the parent is.
+ for (let { parent, child } of validationInfo.problems.parentChildMismatches || []) {
+ // Request both, since we don't know which is right.
+ ids.add(parent);
+ ids.add(child);
+ }
+ if (ids.size > MAX_REQUESTED_IDS) {
+ return ids; // might as well give up here - we aren't going to repair.
+ }
+
+ // Cases where multiple parents reference a child. We re-request both the
+ // child, and all the parents that think they own it. This may be more than
+ // we need, but I don't think we can safely make the assumption that the
+ // child is right.
+ for (let { parents, child } of validationInfo.problems.multipleParents || []) {
+ for (let parent of parents) {
+ ids.add(parent);
+ }
+ ids.add(child);
+ }
+
+ return ids;
+ }
+
+ _countServerOnlyFixableProblems(validationInfo) {
+ const fixableProblems = ["clientMissing", "serverMissing", "serverDeleted"];
+ return fixableProblems.reduce((numProblems, problemLabel) => {
+ return numProblems + validationInfo.problems[problemLabel].length;
+ }, 0);
+ }
+
+ tryServerOnlyRepairs(validationInfo) {
+ if (this._countServerOnlyFixableProblems(validationInfo) == 0) {
+ return false;
+ }
+ let engine = this.service.engineManager.get("bookmarks");
+ for (let id of validationInfo.problems.serverMissing) {
+ engine.addForWeakUpload(id);
+ }
+ let toFetch = engine.toFetch.concat(validationInfo.problems.clientMissing,
+ validationInfo.problems.serverDeleted);
+ engine.toFetch = Array.from(new Set(toFetch));
+ return true;
+ }
+
+ /* See if the repairer is willing and able to begin a repair process given
+ the specified validation information.
+ Returns true if a repair was started and false otherwise.
+ */
+ async startRepairs(validationInfo, flowID) {
+ if (this._currentState != STATE.NOT_REPAIRING) {
+ log.info(`Can't start a repair - repair with ID ${this._flowID} is already in progress`);
+ return false;
+ }
+
+ let ids = this.getProblemIDs(validationInfo);
+ if (ids.size > MAX_REQUESTED_IDS) {
+ log.info("Not starting a repair as there are over " + MAX_REQUESTED_IDS + " problems");
+ let extra = { flowID, reason: `too many problems: ${ids.size}` };
+ this.service.recordTelemetryEvent("repair", "aborted", undefined, extra);
+ return false;
+ }
+
+ if (ids.size == 0) {
+ log.info("Not starting a repair as there are no problems");
+ return false;
+ }
+
+ if (this.anyClientsRepairing()) {
+ log.info("Can't start repair, since other clients are already repairing bookmarks");
+ let extra = { flowID, reason: "other clients repairing" };
+ this.service.recordTelemetryEvent("repair", "aborted", undefined, extra);
+ return false;
+ }
+
+ log.info(`Starting a repair, looking for ${ids.size} missing item(s)`);
+ // setup our prefs to indicate we are on our way.
+ this._flowID = flowID;
+ this._currentIDs = Array.from(ids);
+ this._currentState = STATE.NEED_NEW_CLIENT;
+ this.service.recordTelemetryEvent("repair", "started", undefined, { flowID, numIDs: ids.size.toString() });
+ return this.continueRepairs();
+ }
+
+ /* Work out what state our current repair request is in, and whether it can
+ proceed to a new state.
+ Returns true if we could continue the repair - even if the state didn't
+ actually move. Returns false if we aren't actually repairing.
+ */
+ async continueRepairs(response = null) {
+ // Note that "ABORTED" and "FINISHED" should never be current when this
+ // function returns - this function resets to NOT_REPAIRING in those cases.
+ if (this._currentState == STATE.NOT_REPAIRING) {
+ return false;
+ }
+
+ let state, newState;
+ let abortReason;
+ // we loop until the state doesn't change - but enforce a max of 10 times
+ // to prevent errors causing infinite loops.
+ for (let i = 0; i < 10; i++) {
+ state = this._currentState;
+ log.info("continueRepairs starting with state", state);
+ try {
+ newState = await this._continueRepairs(state, response);
+ log.info("continueRepairs has next state", newState);
+ } catch (ex) {
+ if (!(ex instanceof AbortRepairError)) {
+ throw ex;
+ }
+ log.info(`Repair has been aborted: ${ex.reason}`);
+ newState = STATE.ABORTED;
+ abortReason = ex.reason;
+ }
+
+ if (newState == STATE.ABORTED) {
+ break;
+ }
+
+ this._currentState = newState;
+ Services.prefs.savePrefFile(null); // flush prefs.
+ if (state == newState) {
+ break;
+ }
+ }
+ if (state != newState) {
+ log.error("continueRepairs spun without getting a new state");
+ }
+ if (newState == STATE.FINISHED || newState == STATE.ABORTED) {
+ let object = newState == STATE.FINISHED ? "finished" : "aborted";
+ let extra = {
+ flowID: this._flowID,
+ numIDs: this._currentIDs.length.toString(),
+ };
+ if (abortReason) {
+ extra.reason = abortReason;
+ }
+ this.service.recordTelemetryEvent("repair", object, undefined, extra);
+ // reset our state and flush our prefs.
+ this.prefs.resetBranch();
+ Services.prefs.savePrefFile(null); // flush prefs.
+ }
+ return true;
+ }
+
+ async _continueRepairs(state, response = null) {
+ if (this.anyClientsRepairing(this._flowID)) {
+ throw new AbortRepairError("other clients repairing");
+ }
+ switch (state) {
+ case STATE.SENT_REQUEST:
+ case STATE.SENT_SECOND_REQUEST:
+ let flowID = this._flowID;
+ let clientID = this._currentRemoteClient;
+ if (!clientID) {
+ throw new AbortRepairError(`In state ${state} but have no client IDs listed`);
+ }
+ if (response) {
+ // We got an explicit response - let's see how we went.
+ state = this._handleResponse(state, response);
+ break;
+ }
+ // So we've sent a request - and don't yet have a response. See if the
+ // client we sent it to has removed it from its list (ie, whether it
+ // has synced since we wrote the request.)
+ let client = this.service.clientsEngine.remoteClient(clientID);
+ if (!client) {
+ // hrmph - the client has disappeared.
+ log.info(`previously requested client "${clientID}" has vanished - moving to next step`);
+ state = STATE.NEED_NEW_CLIENT;
+ let extra = {
+ deviceID: this.service.identity.hashedDeviceID(clientID),
+ flowID,
+ };
+ this.service.recordTelemetryEvent("repair", "abandon", "missing", extra);
+ break;
+ }
+ if ((await this._isCommandPending(clientID, flowID))) {
+ // So the command we previously sent is still queued for the client
+ // (ie, that client is yet to have synced). Let's see if we should
+ // give up on that client.
+ let lastRequestSent = this.prefs.get(PREF.REPAIR_WHEN);
+ let timeLeft = lastRequestSent + RESPONSE_INTERVAL_TIMEOUT - this._now();
+ if (timeLeft <= 0) {
+ log.info(`previous request to client ${clientID} is pending, but has taken too long`);
+ state = STATE.NEED_NEW_CLIENT;
+ // XXX - should we remove the command?
+ let extra = {
+ deviceID: this.service.identity.hashedDeviceID(clientID),
+ flowID,
+ };
+ this.service.recordTelemetryEvent("repair", "abandon", "silent", extra);
+ break;
+ }
+ // Let's continue to wait for that client to respond.
+ log.trace(`previous request to client ${clientID} has ${timeLeft} seconds before we give up on it`);
+ break;
+ }
+ // The command isn't pending - if this was the first request, we give
+ // it another go (as that client may have cleared the command but is yet
+ // to complete the sync)
+ // XXX - note that this is no longer true - the responders don't remove
+ // their command until they have written a response. This might mean
+ // we could drop the entire STATE.SENT_SECOND_REQUEST concept???
+ if (state == STATE.SENT_REQUEST) {
+ log.info(`previous request to client ${clientID} was removed - trying a second time`);
+ state = STATE.SENT_SECOND_REQUEST;
+ await this._writeRequest(clientID);
+ } else {
+ // this was the second time around, so give up on this client
+ log.info(`previous 2 requests to client ${clientID} were removed - need a new client`);
+ state = STATE.NEED_NEW_CLIENT;
+ }
+ break;
+
+ case STATE.NEED_NEW_CLIENT:
+ // We need to find a new client to request.
+ let newClientID = this._findNextClient();
+ if (!newClientID) {
+ state = STATE.FINISHED;
+ break;
+ }
+ this._addToPreviousRemoteClients(this._currentRemoteClient);
+ this._currentRemoteClient = newClientID;
+ await this._writeRequest(newClientID);
+ state = STATE.SENT_REQUEST;
+ break;
+
+ case STATE.ABORTED:
+ break; // our caller will take the abort action.
+
+ case STATE.FINISHED:
+ break;
+
+ case STATE.NOT_REPAIRING:
+ // No repair is in progress. This is a common case, so only log trace.
+ log.trace("continue repairs called but no repair in progress.");
+ break;
+
+ default:
+ log.error(`continue repairs finds itself in an unknown state ${state}`);
+ state = STATE.ABORTED;
+ break;
+
+ }
+ return state;
+ }
+
+ /* Handle being in the SENT_REQUEST or SENT_SECOND_REQUEST state with an
+ explicit response.
+ */
+ _handleResponse(state, response) {
+ let clientID = this._currentRemoteClient;
+ let flowID = this._flowID;
+
+ if (response.flowID != flowID || response.clientID != clientID ||
+ response.request != "upload") {
+ log.info("got a response to a different repair request", response);
+ // hopefully just a stale request that finally came in (either from
+ // an entirely different repair flow, or from a client we've since
+ // given up on.) It doesn't mean we need to abort though...
+ return state;
+ }
+ // Pull apart the response and see if it provided everything we asked for.
+ let remainingIDs = Array.from(CommonUtils.difference(this._currentIDs, response.ids));
+ log.info(`repair response from ${clientID} provided "${response.ids}", remaining now "${remainingIDs}"`);
+ this._currentIDs = remainingIDs;
+ if (remainingIDs.length) {
+ // try a new client for the remaining ones.
+ state = STATE.NEED_NEW_CLIENT;
+ } else {
+ state = STATE.FINISHED;
+ }
+ // record telemetry about this
+ let extra = {
+ deviceID: this.service.identity.hashedDeviceID(clientID),
+ flowID,
+ numIDs: response.ids.length.toString(),
+ };
+ this.service.recordTelemetryEvent("repair", "response", "upload", extra);
+ return state;
+ }
+
+ /* Issue a repair request to a specific client.
+ */
+ async _writeRequest(clientID) {
+ log.trace("writing repair request to client", clientID);
+ let ids = this._currentIDs;
+ if (!ids) {
+ throw new AbortRepairError("Attempting to write a request, but there are no IDs");
+ }
+ let flowID = this._flowID;
+ // Post a command to that client.
+ let request = {
+ collection: "bookmarks",
+ request: "upload",
+ requestor: this.service.clientsEngine.localID,
+ ids,
+ flowID,
+ };
+ await this.service.clientsEngine.sendCommand("repairRequest", [request], clientID, { flowID });
+ this.prefs.set(PREF.REPAIR_WHEN, Math.floor(this._now()));
+ // record telemetry about this
+ let extra = {
+ deviceID: this.service.identity.hashedDeviceID(clientID),
+ flowID,
+ numIDs: ids.length.toString(),
+ };
+ this.service.recordTelemetryEvent("repair", "request", "upload", extra);
+ }
+
+ _findNextClient() {
+ let alreadyDone = this._getPreviousRemoteClients();
+ alreadyDone.push(this._currentRemoteClient);
+ let remoteClients = this.service.clientsEngine.remoteClients;
+ // we want to consider the most-recently synced clients first.
+ remoteClients.sort((a, b) => b.serverLastModified - a.serverLastModified);
+ for (let client of remoteClients) {
+ log.trace("findNextClient considering", client);
+ if (alreadyDone.indexOf(client.id) == -1 && this._isSuitableClient(client)) {
+ return client.id;
+ }
+ }
+ log.trace("findNextClient found no client");
+ return null;
+ }
+
+ /* Is the passed client record suitable as a repair responder?
+ */
+ _isSuitableClient(client) {
+ // filter only desktop firefox running > 53 (ie, any 54)
+ return (client.type == DEVICE_TYPE_DESKTOP &&
+ Services.vc.compare(client.version, 53) > 0);
+ }
+
+ /* Is our command still in the "commands" queue for the specific client?
+ */
+ async _isCommandPending(clientID, flowID) {
+ // getClientCommands() is poorly named - it's only outgoing commands
+ // from us we have yet to write. For our purposes, we want to check
+ // them and commands previously written (which is in .commands)
+ let clientCommands = await this.service.clientsEngine.getClientCommands(clientID);
+ let commands = [...clientCommands,
+ ...this.service.clientsEngine.remoteClient(clientID).commands || []];
+ for (let command of commands) {
+ if (command.command != "repairRequest" || command.args.length != 1) {
+ continue;
+ }
+ let arg = command.args[0];
+ if (arg.collection == "bookmarks" && arg.request == "upload" &&
+ arg.flowID == flowID) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // accessors for our prefs.
+ get _currentState() {
+ return this.prefs.get(PREF.CURRENT_STATE, STATE.NOT_REPAIRING);
+ }
+ set _currentState(newState) {
+ this.prefs.set(PREF.CURRENT_STATE, newState);
+ }
+
+ get _currentIDs() {
+ let ids = this.prefs.get(PREF.REPAIR_MISSING_IDS, "");
+ return ids.length ? ids.split(" ") : [];
+ }
+ set _currentIDs(arrayOfIDs) {
+ this.prefs.set(PREF.REPAIR_MISSING_IDS, arrayOfIDs.join(" "));
+ }
+
+ get _currentRemoteClient() {
+ return this.prefs.get(PREF.REPAIR_CURRENT_CLIENT);
+ }
+ set _currentRemoteClient(clientID) {
+ this.prefs.set(PREF.REPAIR_CURRENT_CLIENT, clientID);
+ }
+
+ get _flowID() {
+ return this.prefs.get(PREF.REPAIR_ID);
+ }
+ set _flowID(val) {
+ this.prefs.set(PREF.REPAIR_ID, val);
+ }
+
+ // use a function for this pref to offer somewhat sane semantics.
+ _getPreviousRemoteClients() {
+ let alreadyDone = this.prefs.get(PREF.REPAIR_PREVIOUS_CLIENTS, "");
+ return alreadyDone.length ? alreadyDone.split(" ") : [];
+ }
+ _addToPreviousRemoteClients(clientID) {
+ let arrayOfClientIDs = this._getPreviousRemoteClients();
+ arrayOfClientIDs.push(clientID);
+ this.prefs.set(PREF.REPAIR_PREVIOUS_CLIENTS, arrayOfClientIDs.join(" "));
+ }
+
+ /* Used for test mocks.
+ */
+ _now() {
+ // We use the server time, which is SECONDS
+ return Resource.serverTime;
+ }
+}
+
+/* An object that responds to repair requests initiated by some other device.
+*/
+class BookmarkRepairResponder extends CollectionRepairResponder {
+ async repair(request, rawCommand) {
+ if (request.request != "upload") {
+ await this._abortRepair(request, rawCommand,
+ `Don't understand request type '${request.request}'`);
+ return;
+ }
+
+ // Note that we don't try and guard against multiple repairs being in
+ // progress as we don't do anything too smart that could cause problems,
+ // but just upload items. If we get any smarter we should re-think this
+ // (but when we do, note that checking this._currentState isn't enough as
+ // this responder is not a singleton)
+
+ this._currentState = {
+ request,
+ rawCommand,
+ processedCommand: false,
+ ids: [],
+ };
+
+ try {
+ let engine = this.service.engineManager.get("bookmarks");
+ let { toUpload, toDelete } = await this._fetchItemsToUpload(request);
+
+ if (toUpload.size || toDelete.size) {
+ log.debug(`repair request will upload ${toUpload.size} items and delete ${toDelete.size} items`);
+ // whew - now add these items to the tracker "weakly" (ie, they will not
+ // persist in the case of a restart, but that's OK - we'll then end up here
+ // again) and also record them in the response we send back.
+ for (let id of toUpload) {
+ engine.addForWeakUpload(id);
+ this._currentState.ids.push(id);
+ }
+ for (let id of toDelete) {
+ engine.addForWeakUpload(id, { forceTombstone: true });
+ this._currentState.ids.push(id);
+ }
+
+ // We have arranged for stuff to be uploaded, so wait until that's done.
+ Svc.Obs.add("weave:engine:sync:uploaded", this.onUploaded, this);
+ // and record in telemetry that we got this far - just incase we never
+ // end up doing the upload for some obscure reason.
+ let eventExtra = {
+ flowID: request.flowID,
+ numIDs: this._currentState.ids.length.toString(),
+ };
+ this.service.recordTelemetryEvent("repairResponse", "uploading", undefined, eventExtra);
+ } else {
+ // We were unable to help with the repair, so report that we are done.
+ await this._finishRepair();
+ }
+ } catch (ex) {
+ if (Async.isShutdownException(ex)) {
+ // this repair request will be tried next time.
+ throw ex;
+ }
+ // On failure, we still write a response so the requestor knows to move
+ // on, but we record the failure reason in telemetry.
+ log.error("Failed to respond to the repair request", ex);
+ this._currentState.failureReason = SyncTelemetry.transformError(ex);
+ await this._finishRepair();
+ }
+ }
+
+ async _fetchItemsToUpload(request) {
+ let toUpload = new Set(); // items we will upload.
+ let toDelete = new Set(); // items we will delete.
+
+ let requested = new Set(request.ids);
+
+ let engine = this.service.engineManager.get("bookmarks");
+ // Determine every item that may be impacted by the requested IDs - eg,
+ // this may include children if a requested ID is a folder.
+ // Turn an array of { recordId, syncable } into a map of recordId -> syncable.
+ let repairable = await PlacesSyncUtils.bookmarks.fetchRecordIdsForRepair(request.ids);
+ if (repairable.length == 0) {
+ // server will get upset if we request an empty set, and we can't do
+ // anything in that case, so bail now.
+ return { toUpload, toDelete };
+ }
+
+ // which of these items exist on the server?
+ let itemSource = engine.itemSource();
+ itemSource.ids = repairable.map(item => item.recordId);
+ log.trace(`checking the server for items`, itemSource.ids);
+ let itemsResponse = await itemSource.get();
+ // If the response failed, don't bother trying to parse the output.
+ // Throwing here means we abort the repair, which isn't ideal for transient
+ // errors (eg, no network, 500 service outage etc), but we don't currently
+ // have a sane/safe way to try again later (we'd need to implement a kind
+ // of timeout, otherwise we might end up retrying forever and never remove
+ // our request command.) Bug 1347805.
+ if (!itemsResponse.success) {
+ throw new Error(`request for server IDs failed: ${itemsResponse.status}`);
+ }
+ let existRemotely = new Set(JSON.parse(itemsResponse));
+ // We need to be careful about handing the requested items:
+ // * If the item exists locally but isn't in the tree of items we sync
+ // (eg, it might be a left-pane item or similar, we write a tombstone.
+ // * If the item exists locally as a folder, we upload the folder and any
+ // children which don't exist on the server. (Note that we assume the
+ // parents *do* exist)
+ // Bug 1343101 covers additional issues we might repair in the future.
+ for (let { recordId: id, syncable } of repairable) {
+ if (requested.has(id)) {
+ if (syncable) {
+ log.debug(`repair request to upload item '${id}' which exists locally; uploading`);
+ toUpload.add(id);
+ } else {
+ // explicitly requested and not syncable, so tombstone.
+ log.debug(`repair request to upload item '${id}' but it isn't under a syncable root; writing a tombstone`);
+ toDelete.add(id);
+ }
+ // The item wasn't explicitly requested - only upload if it is syncable
+ // and doesn't exist on the server.
+ } else if (syncable && !existRemotely.has(id)) {
+ log.debug(`repair request found related item '${id}' which isn't on the server; uploading`);
+ toUpload.add(id);
+ } else if (!syncable && existRemotely.has(id)) {
+ log.debug(`repair request found non-syncable related item '${id}' on the server; writing a tombstone`);
+ toDelete.add(id);
+ } else {
+ log.debug(`repair request found related item '${id}' which we will not upload; ignoring`);
+ }
+ }
+ return { toUpload, toDelete };
+ }
+
+ onUploaded(subject, data) {
+ if (data != "bookmarks") {
+ return;
+ }
+ Svc.Obs.remove("weave:engine:sync:uploaded", this.onUploaded, this);
+ if (subject.failed) {
+ return;
+ }
+ log.debug(`bookmarks engine has uploaded stuff - creating a repair response`, subject);
+ Async.promiseSpinningly(this._finishRepair());
+ }
+
+ async _finishRepair() {
+ let clientsEngine = this.service.clientsEngine;
+ let flowID = this._currentState.request.flowID;
+ let response = {
+ request: this._currentState.request.request,
+ collection: "bookmarks",
+ clientID: clientsEngine.localID,
+ flowID,
+ ids: this._currentState.ids,
+ };
+ let clientID = this._currentState.request.requestor;
+ await clientsEngine.sendCommand("repairResponse", [response], clientID, { flowID });
+ // and nuke the request from our client.
+ await clientsEngine.removeLocalCommand(this._currentState.rawCommand);
+ let eventExtra = {
+ flowID,
+ numIDs: response.ids.length.toString(),
+ };
+ if (this._currentState.failureReason) {
+ // *sob* - recording this in "extra" means the value must be a string of
+ // max 85 chars.
+ eventExtra.failureReason = JSON.stringify(this._currentState.failureReason).substring(0, 85);
+ this.service.recordTelemetryEvent("repairResponse", "failed", undefined, eventExtra);
+ } else {
+ this.service.recordTelemetryEvent("repairResponse", "finished", undefined, eventExtra);
+ }
+ this._currentState = null;
+ }
+
+ async _abortRepair(request, rawCommand, why) {
+ log.warn(`aborting repair request: ${why}`);
+ await this.service.clientsEngine.removeLocalCommand(rawCommand);
+ // record telemetry for this.
+ let eventExtra = {
+ flowID: request.flowID,
+ reason: why,
+ };
+ this.service.recordTelemetryEvent("repairResponse", "aborted", undefined, eventExtra);
+ // We could also consider writing a response here so the requestor can take
+ // some immediate action rather than timing out, but we abort only in cases
+ // that should be rare, so let's wait and see what telemetry tells us.
+ }
+}
+
+/* Exposed in case another module needs to understand our state.
+*/
+BookmarkRepairRequestor.STATE = STATE;
+BookmarkRepairRequestor.PREF = PREF;
rename from services/sync/modules/bookmark_validator.js
rename to browser/extensions/sync-repair/bookmark_validator.jsm
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/bootstrap.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported startup, shutdown, install, uninstall */
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.importGlobalProperties(["fetch"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Weave", "resource://services-sync/main.js");
+XPCOMUtils.defineLazyModuleGetter(this, "RepairManager", "resource://sync-repair/repair_manager.jsm");
+
+const Loader = {
+ // Used to handle the case where we unregister before whenLoaded() from a
+ // previous call to `register()` fires.
+ _pendingPromise: Promise.resolve(),
+
+ // This is done so that we can unload components loaded with Cu.import, so
+ // that we can properly update without restart.
+ _jsms: new Set(),
+ async _populateJSMs() {
+ // This is a bit awkward, but it gets a listing of the resource
+ // folder, and uses it to find out what we'll need to unload later.
+ let dirListText = await fetch("resource://sync-repair/").then(r => r.text());
+ // First two lines are headings.
+ let dirList = dirListText.split("\n").slice(2);
+ // Each line is more or less in the format `num: filename.ext size timestamp type`
+ // we only care about the filename portion, and only if it is a JSM.
+ let jsmFilenames = dirList.map(x => x.split(" ")[1]).filter(x => x && x.match(/\.jsm$/i));
+ this._jsms = new Set(jsmFilenames.map(filename => `resource://sync-repair/${filename}`));
+ },
+
+ _unloadJSMs() {
+ for (let jsm of this._jsms) {
+ Cu.unload(jsm);
+ }
+ },
+
+ register() {
+ return this._pendingPromise = this._pendingPromise
+ .then(() => this._whenSyncLoaded())
+ .then(() => this._populateJSMs())
+ .then(() => Weave.Service.setRepairManager(new RepairManager()))
+ .catch(Cu.reportError);
+ },
+
+ unregister() {
+ return this._pendingPromise = this._pendingPromise
+ .then(() => this._whenSyncLoaded())
+ .then(() => Weave.Service.clearRepairManager())
+ .then(() => this._unloadJSMs())
+ .catch(Cu.reportError);
+ },
+
+ _whenSyncLoaded() {
+ return Cc["@mozilla.org/weave/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject
+ .whenLoaded();
+ },
+};
+
+function startup(data) {
+ Loader.register();
+}
+
+function shutdown() {
+ Loader.unregister();
+}
+
+function install() {}
+function uninstall() {}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/collection_repair.jsm
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-sync/main.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkRepairRequestor",
+ "resource://services-sync/bookmark_repair.jsm");
+
+this.EXPORTED_SYMBOLS = ["getRepairRequestor", "getAllRepairRequestors",
+ "CollectionRepairRequestor",
+ "getRepairResponder",
+ "CollectionRepairResponder",
+ "getValidator"];
+
+// The individual requestors/responders/validators, lazily loaded.
+const REQUESTORS = {
+ bookmarks: ["bookmark_repair.jsm", "BookmarkRepairRequestor"],
+};
+
+const RESPONDERS = {
+ bookmarks: ["bookmark_repair.jsm", "BookmarkRepairResponder"],
+};
+
+const VALIDATORS = {
+ addons: ["addon_validator.jsm", "AddonValidator"],
+ bookmarks: ["bookmark_validator.jsm", "BookmarkValidator"],
+ forms: ["form_validator.jsm", "FormValidator"],
+ passwords: ["password_validator.jsm", "PasswordValidator"],
+};
+
+function _getRepairConstructor(which, collection) {
+ if (!(collection in which)) {
+ return null;
+ }
+ let [modname, symbolname] = which[collection];
+ let ns = {};
+ Cu.import("resource://sync-repair/" + modname, ns);
+ return ns[symbolname];
+}
+
+function getRepairRequestor(collection) {
+ let ctor = _getRepairConstructor(REQUESTORS, collection);
+ if (!ctor) {
+ return null;
+ }
+ return new ctor();
+}
+
+function getAllRepairRequestors() {
+ let result = {};
+ for (let collection of Object.keys(REQUESTORS)) {
+ let ctor = _getRepairConstructor(REQUESTORS, collection);
+ result[collection] = new ctor();
+ }
+ return result;
+}
+
+function getRepairResponder(collection) {
+ let ctor = _getRepairConstructor(RESPONDERS, collection);
+ if (!ctor) {
+ return null;
+ }
+ return new ctor();
+}
+
+function getValidator(collection) {
+ let ctor = _getRepairConstructor(VALIDATORS, collection);
+ if (!ctor) {
+ return null;
+ }
+ return new ctor();
+}
+
+// The abstract classes.
+class CollectionRepairRequestor {
+ constructor(service = null) {
+ // allow service to be mocked in tests.
+ this.service = service || Weave.Service;
+ }
+
+ /* Try to resolve some issues with the server without involving other clients.
+ Returns true if we repaired some items.
+
+ @param validationInfo {Object}
+ The validation info as returned by the collection's validator.
+
+ */
+ tryServerOnlyRepairs(validationInfo) {
+ return false;
+ }
+
+ /* See if the repairer is willing and able to begin a repair process given
+ the specified validation information.
+ Returns true if a repair was started and false otherwise.
+
+ @param validationInfo {Object}
+ The validation info as returned by the collection's validator.
+
+ @param flowID {String}
+ A guid that uniquely identifies this repair process for this
+ collection, and which should be sent to any requestors and
+ reported in telemetry.
+
+ */
+ async startRepairs(validationInfo, flowID) {
+ throw new Error("not implemented");
+ }
+
+ /* Work out what state our current repair request is in, and whether it can
+ proceed to a new state.
+ Returns true if we could continue the repair - even if the state didn't
+ actually move. Returns false if we aren't actually repairing.
+
+ @param responseInfo {Object}
+ An optional response to a previous repair request, as returned
+ by a remote repair responder.
+
+ */
+ async continueRepairs(responseInfo = null) {
+ throw new Error("not implemented");
+ }
+}
+
+class CollectionRepairResponder {
+ constructor(service = null) {
+ // allow service to be mocked in tests.
+ this.service = service || Weave.Service;
+ }
+
+ /* Take some action in response to a repair request. Returns a promise that
+ resolves once the repair process has started, or rejects if there
+ was an error starting the repair.
+
+ Note that when the promise resolves the repair is not yet complete - at
+ some point in the future the repair will auto-complete, at which time
+ |rawCommand| will be removed from the list of client commands for this
+ client.
+
+ @param request {Object}
+ The repair request as sent by another client.
+
+ @param rawCommand {Object}
+ The command object as stored in the clients engine, and which
+ will be automatically removed once a repair completes.
+ */
+ async repair(request, rawCommand) {
+ throw new Error("not implemented");
+ }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/collection_validator.jsm
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Async",
+ "resource://services-common/async.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Weave", "resource://services-sync/main.js");
+
+this.EXPORTED_SYMBOLS = ["CollectionValidator", "CollectionProblemData"];
+
+class CollectionProblemData {
+ constructor() {
+ this.missingIDs = 0;
+ this.duplicates = [];
+ this.clientMissing = [];
+ this.serverMissing = [];
+ this.serverDeleted = [];
+ this.serverUnexpected = [];
+ this.differences = [];
+ }
+
+ /**
+ * Produce a list summarizing problems found. Each entry contains {name, count},
+ * where name is the field name for the problem, and count is the number of times
+ * the problem was encountered.
+ *
+ * Validation has failed if all counts are not 0.
+ */
+ getSummary() {
+ return [
+ { name: "clientMissing", count: this.clientMissing.length },
+ { name: "serverMissing", count: this.serverMissing.length },
+ { name: "serverDeleted", count: this.serverDeleted.length },
+ { name: "serverUnexpected", count: this.serverUnexpected.length },
+ { name: "differences", count: this.differences.length },
+ { name: "missingIDs", count: this.missingIDs },
+ { name: "duplicates", count: this.duplicates.length }
+ ];
+ }
+}
+
+class CollectionValidator {
+ // Construct a generic collection validator. This is intended to be called by
+ // subclasses.
+ // - name: Name of the engine
+ // - idProp: Property that identifies a record. That is, if a client and server
+ // record have the same value for the idProp property, they should be
+ // compared against eachother.
+ // - props: Array of properties that should be compared
+ constructor(name, idProp, props) {
+ this.name = name;
+ this.props = props;
+ this.idProp = idProp;
+
+ // This property deals with the fact that form history records are never
+ // deleted from the server. The FormValidator subclass needs to ignore the
+ // client missing records, and it uses this property to achieve it -
+ // (Bug 1354016).
+ this.ignoresMissingClients = false;
+ this.engine = Weave.Service.engineManager.get(name);
+ }
+
+ // Should a custom ProblemData type be needed, return it here.
+ emptyProblemData() {
+ return new CollectionProblemData();
+ }
+
+ async getServerItems(engine) {
+ let collection = engine.itemSource();
+ let collectionKey = engine.service.collectionKeys.keyForCollection(engine.name);
+ collection.full = true;
+ let result = await collection.getBatched();
+ if (!result.response.success) {
+ throw result.response;
+ }
+ let maybeYield = Async.jankYielder();
+ let cleartexts = [];
+ for (let record of result.records) {
+ await maybeYield();
+ await record.decrypt(collectionKey);
+ cleartexts.push(record.cleartext);
+ }
+ return cleartexts;
+ }
+
+ // Should return a promise that resolves to an array of client items.
+ getClientItems() {
+ return Promise.reject("Must implement");
+ }
+
+ /**
+ * Can we guarantee validation will fail with a reason that isn't actually a
+ * problem? For example, if we know there are pending changes left over from
+ * the last sync, this should resolve to false. By default resolves to true.
+ */
+ async canValidate() {
+ return true;
+ }
+
+ // Turn the client item into something that can be compared with the server item,
+ // and is also safe to mutate.
+ normalizeClientItem(item) {
+ return Cu.cloneInto(item, {});
+ }
+
+ // Turn the server item into something that can be easily compared with the client
+ // items.
+ async normalizeServerItem(item) {
+ return item;
+ }
+
+ // Return whether or not a server item should be present on the client. Expected
+ // to be overridden.
+ clientUnderstands(item) {
+ return true;
+ }
+
+ // Return whether or not a client item should be present on the server. Expected
+ // to be overridden
+ syncedByClient(item) {
+ return true;
+ }
+
+ // Compare the server item and the client item, and return a list of property
+ // names that are different. Can be overridden if needed.
+ getDifferences(client, server) {
+ let differences = [];
+ for (let prop of this.props) {
+ let clientProp = client[prop];
+ let serverProp = server[prop];
+ if ((clientProp || "") !== (serverProp || "")) {
+ differences.push(prop);
+ }
+ }
+ return differences;
+ }
+
+ // Returns an object containing
+ // problemData: an instance of the class returned by emptyProblemData(),
+ // clientRecords: Normalized client records
+ // records: Normalized server records,
+ // deletedRecords: Array of ids that were marked as deleted by the server.
+ async compareClientWithServer(clientItems, serverItems) {
+ let maybeYield = Async.jankYielder();
+ const clientRecords = [];
+ for (let item of clientItems) {
+ await maybeYield();
+ clientRecords.push(this.normalizeClientItem(item));
+ }
+ const serverRecords = [];
+ for (let item of serverItems) {
+ await maybeYield();
+ serverRecords.push((await this.normalizeServerItem(item)));
+ }
+ let problems = this.emptyProblemData();
+ let seenServer = new Map();
+ let serverDeleted = new Set();
+ let allRecords = new Map();
+
+ for (let record of serverRecords) {
+ let id = record[this.idProp];
+ if (!id) {
+ ++problems.missingIDs;
+ continue;
+ }
+ if (record.deleted) {
+ serverDeleted.add(record);
+ } else {
+ let possibleDupe = seenServer.get(id);
+ if (possibleDupe) {
+ problems.duplicates.push(id);
+ } else {
+ seenServer.set(id, record);
+ allRecords.set(id, { server: record, client: null, });
+ }
+ record.understood = this.clientUnderstands(record);
+ }
+ }
+
+ let seenClient = new Map();
+ for (let record of clientRecords) {
+ let id = record[this.idProp];
+ record.shouldSync = this.syncedByClient(record);
+ seenClient.set(id, record);
+ let combined = allRecords.get(id);
+ if (combined) {
+ combined.client = record;
+ } else {
+ allRecords.set(id, { client: record, server: null });
+ }
+ }
+
+ for (let [id, { server, client }] of allRecords) {
+ if (!client && !server) {
+ throw new Error("Impossible: no client or server record for " + id);
+ } else if (server && !client) {
+ if (!this.ignoresMissingClients && server.understood) {
+ problems.clientMissing.push(id);
+ }
+ } else if (client && !server) {
+ if (client.shouldSync) {
+ problems.serverMissing.push(id);
+ }
+ } else {
+ if (!client.shouldSync) {
+ if (!problems.serverUnexpected.includes(id)) {
+ problems.serverUnexpected.push(id);
+ }
+ continue;
+ }
+ let differences = this.getDifferences(client, server);
+ if (differences && differences.length) {
+ problems.differences.push({ id, differences });
+ }
+ }
+ }
+ return {
+ problemData: problems,
+ clientRecords,
+ records: serverRecords,
+ deletedRecords: [...serverDeleted]
+ };
+ }
+}
+
+// Default to 0, some engines may override.
+CollectionValidator.prototype.version = 0;
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/doctor.jsm
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A doctor for our collections. It can be asked to make a consultation, and
+// may just diagnose an issue without attempting to cure it, may diagnose and
+// attempt to cure, or may decide it is overworked and underpaid.
+// Or something - naming is hard :)
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Doctor"];
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/resource.js");
+Cu.import("resource://services-sync/util.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "getRepairRequestor",
+ "resource://sync-repair/collection_repair.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "getAllRepairRequestors",
+ "resource://sync-repair/collection_repair.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "getValidator",
+ "resource://sync-repair/collection_repair.jsm");
+
+const log = Log.repository.getLogger("Sync.Repair.Doctor");
+
+this.REPAIR_ADVANCE_PERIOD = 86400; // 1 day
+
+const ValidatedEngines = [
+ { name: "bookmarks", symbol: "BookmarkValidator", module: "resource://bookmark_validator.jsm" }
+];
+
+this.Doctor = {
+ anyClientsRepairing(service, collection, ignoreFlowID = null) {
+ if (!service || !service.clientsEngine) {
+ log.info("Missing clients engine, assuming we're in test code");
+ return false;
+ }
+ return service.clientsEngine.remoteClients.some(client =>
+ client.commands && client.commands.some(command => {
+ if (command.command != "repairResponse" && command.command != "repairRequest") {
+ return false;
+ }
+ if (!command.args || command.args.length != 1) {
+ return false;
+ }
+ if (command.args[0].collection != collection) {
+ return false;
+ }
+ if (ignoreFlowID != null && command.args[0].flowID == ignoreFlowID) {
+ return false;
+ }
+ return true;
+ })
+ );
+ },
+
+ async consult(recentlySyncedEngines) {
+ if (!Services.telemetry.canRecordBase) {
+ log.info("Skipping consultation: telemetry reporting is disabled");
+ return;
+ }
+
+ let engineInfos = this._getEnginesToValidate(recentlySyncedEngines);
+
+ await this._runValidators(engineInfos);
+
+ // We are called at the end of a sync, which is a good time to periodically
+ // check each repairer to see if it can advance.
+ if (this._now() - this.lastRepairAdvance > REPAIR_ADVANCE_PERIOD) {
+ try {
+ for (let [collection, requestor] of Object.entries(this._getAllRepairRequestors())) {
+ try {
+ let advanced = await requestor.continueRepairs();
+ log.info(`${collection} reparier ${advanced ? "advanced" : "did not advance"}.`);
+ } catch (ex) {
+ if (Async.isShutdownException(ex)) {
+ throw ex;
+ }
+ log.error(`${collection} repairer failed`, ex);
+ }
+ }
+ } finally {
+ this.lastRepairAdvance = Math.floor(this._now());
+ }
+ }
+ },
+
+ _getEnginesToValidate(recentlySyncedEngines) {
+ let result = {};
+ for (let e of recentlySyncedEngines) {
+ let prefPrefix = `engine.${e.name}.`;
+ if (!Svc.Prefs.get(prefPrefix + "validation.enabled", false)) {
+ log.info(`Skipping check of ${e.name} - disabled via preferences`);
+ continue;
+ }
+ // Check the last validation time for the engine.
+ let lastValidation = Svc.Prefs.get(prefPrefix + "validation.lastTime", 0);
+ let validationInterval = Svc.Prefs.get(prefPrefix + "validation.interval");
+ let nowSeconds = this._now();
+
+ if (nowSeconds - lastValidation < validationInterval) {
+ log.info(`Skipping validation of ${e.name}: too recent since last validation attempt`);
+ continue;
+ }
+ // Update the time now, even if we decline to actually perform a
+ // validation. We don't want to check the rest of these more frequently
+ // than once a day.
+ Svc.Prefs.set(prefPrefix + "validation.lastTime", Math.floor(nowSeconds));
+
+ // Validation only occurs a certain percentage of the time.
+ let validationProbability = Svc.Prefs.get(prefPrefix + "validation.percentageChance", 0) / 100.0;
+ if (validationProbability < Math.random()) {
+ log.info(`Skipping validation of ${e.name}: Probability threshold not met`);
+ continue;
+ }
+
+ let maxRecords = Svc.Prefs.get(prefPrefix + "validation.maxRecords");
+ if (!maxRecords) {
+ log.info(`Skipping validation of ${e.name}: No maxRecords specified`);
+ continue;
+ }
+ // OK, so this is a candidate - the final decision will be based on the
+ // number of records actually found.
+ result[e.name] = { engine: e, maxRecords };
+ }
+ return result;
+ },
+
+ async _runValidators(engineInfos) {
+ if (Object.keys(engineInfos).length == 0) {
+ log.info("Skipping validation: no engines qualify");
+ return;
+ }
+
+ if (Object.values(engineInfos).filter(i => i.maxRecords != -1).length != 0) {
+ // at least some of the engines have maxRecord restrictions which require
+ // us to ask the server for the counts.
+ let countInfo = await this._fetchCollectionCounts();
+ for (let [engineName, recordCount] of Object.entries(countInfo)) {
+ if (engineName in engineInfos) {
+ engineInfos[engineName].recordCount = recordCount;
+ }
+ }
+ }
+
+ for (let [engineName, { engine, maxRecords, recordCount }] of Object.entries(engineInfos)) {
+ // maxRecords of -1 means "any number", so we can skip asking the server.
+ // Used for tests.
+ if (maxRecords >= 0 && recordCount > maxRecords) {
+ log.debug(`Skipping validation for ${engineName} because ` +
+ `the number of records (${recordCount}) is greater ` +
+ `than the maximum allowed (${maxRecords}).`);
+ continue;
+ }
+ let validator = this._getValidator(engine.name);
+ if (!validator) {
+ continue;
+ }
+
+ if (!await validator.canValidate()) {
+ log.debug(`Skipping validation for ${engineName} because validator.canValidate() is false`);
+ continue;
+ }
+
+ // Let's do it!
+ Services.console.logStringMessage(
+ `Sync is about to run a consistency check of ${engine.name}. This may be slow, and ` +
+ `can be controlled using the pref "services.sync.${engine.name}.validation.enabled".\n` +
+ `If you encounter any problems because of this, please file a bug.`);
+
+ // Make a new flowID just incase we end up starting a repair.
+ let flowID = Utils.makeGUID();
+ try {
+ // XXX - must get the flow ID to either the validator, or directly to
+ // telemetry. I guess it's probably OK to always record a flowID even
+ // if we don't end up repairing?
+ log.info(`Running validator for ${engine.name}`);
+ let result = await validator.validate(engine);
+ Observers.notify("weave:engine:validate:finish", result, engine.name);
+ // And see if we should repair.
+ await this._maybeCure(engine, result, flowID);
+ } catch (ex) {
+ if (Async.isShutdownException(ex)) {
+ throw ex;
+ }
+ log.error(`Failed to run validation on ${engine.name}!`, ex);
+ Observers.notify("weave:engine:validate:error", ex, engine.name);
+ // Keep validating -- there's no reason to think that a failure for one
+ // validator would mean the others will fail.
+ }
+ }
+ },
+
+ // Separated to allow easier mocking during tests
+ _getValidator(name) {
+ return getValidator(name);
+ },
+
+ async _maybeCure(engine, validationResults, flowID) {
+ if (!this._shouldRepair(engine)) {
+ log.info(`Skipping repair of ${engine.name} - disabled via preferences`);
+ return;
+ }
+
+ let requestor = this._getRepairRequestor(engine.name);
+ let didStart = false;
+ if (requestor) {
+ if (requestor.tryServerOnlyRepairs(validationResults)) {
+ return; // TODO: It would be nice if we could request a validation to be
+ // done on next sync.
+ }
+ didStart = await requestor.startRepairs(validationResults, flowID);
+ }
+ log.info(`${didStart ? "did" : "didn't"} start a repair of ${engine.name} with flowID ${flowID}`);
+ },
+
+ _shouldRepair(engine) {
+ return Svc.Prefs.get(`engine.${engine.name}.repair.enabled`, false);
+ },
+
+ // mainly for mocking.
+ async _fetchCollectionCounts() {
+ let collectionCountsURL = Service.userBaseURL + "info/collection_counts";
+ try {
+ let infoResp = await Service._fetchInfo(collectionCountsURL);
+ if (!infoResp.success) {
+ log.error("Can't fetch collection counts: request to info/collection_counts responded with "
+ + infoResp.status);
+ return {};
+ }
+ return infoResp.obj; // might throw because obj is a getter which parses json.
+ } catch (ex) {
+ if (Async.isShutdownException(ex)) {
+ throw ex;
+ }
+ // Not running validation is totally fine, so we just write an error log and return.
+ log.error("Caught error when fetching counts", ex);
+ return {};
+ }
+ },
+
+ get lastRepairAdvance() {
+ return Svc.Prefs.get("doctor.lastRepairAdvance", 0);
+ },
+
+ set lastRepairAdvance(value) {
+ Svc.Prefs.set("doctor.lastRepairAdvance", value);
+ },
+
+ // functions used so tests can mock them
+ _now() {
+ // We use the server time, which is SECONDS
+ return Resource.serverTime;
+ },
+
+ _getRepairRequestor(name) {
+ return getRepairRequestor(name);
+ },
+
+ _getAllRepairRequestors() {
+ return getAllRepairRequestors();
+ }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/form_validator.jsm
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://sync-repair/collection_validator.jsm");
+Cu.import("resource://services-sync/engines/forms.js");
+
+this.EXPORTED_SYMBOLS = ["FormValidator"];
+
+class FormsProblemData extends CollectionProblemData {
+ getSummary() {
+ // We don't support syncing deleted form data, so "clientMissing" isn't a problem
+ return super.getSummary().filter(entry =>
+ entry.name !== "clientMissing");
+ }
+}
+
+class FormValidator extends CollectionValidator {
+ constructor() {
+ super("forms", "id", ["name", "value"]);
+ this.ignoresMissingClients = true;
+ }
+
+ emptyProblemData() {
+ return new FormsProblemData();
+ }
+
+ async getClientItems() {
+ return this.engine._search(["guid", "fieldname", "value"], {});
+ }
+
+ normalizeClientItem(item) {
+ return {
+ id: item.guid,
+ guid: item.guid,
+ name: item.fieldname,
+ fieldname: item.fieldname,
+ value: item.value,
+ original: item,
+ };
+ }
+
+ async normalizeServerItem(item) {
+ let res = Object.assign({
+ guid: item.id,
+ fieldname: item.name,
+ original: item,
+ }, item);
+ // Missing `name` or `value` causes the getGUID call to throw
+ if (item.name !== undefined && item.value !== undefined) {
+ let guid = await this.engine._getGUID(item.name, item.value);
+ if (guid) {
+ res.guid = guid;
+ res.id = guid;
+ res.duped = true;
+ }
+ }
+
+ return res;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/install.rdf.in
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:name>Sync Validation and Repair</em:name>
+ <em:id>sync-repair@mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+ <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+ <!-- Target Application this extension can install into,
+ with minimum and maximum supported versions. -->
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+ <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Using jsm files allows us to more easily glob these here
+[features/sync-repair@mozilla.org] chrome.jar:
+% resource sync-repair %res/
+ res/ (*.jsm)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_FILES.features['sync-repair@mozilla.org'] += [
+ 'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['sync-repair@mozilla.org'] += [
+ 'install.rdf.in'
+]
+
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Sync')
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/password_validator.jsm
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://sync-repair/collection_validator.jsm");
+
+this.EXPORTED_SYMBOLS = ["PasswordValidator"];
+
+class PasswordValidator extends CollectionValidator {
+ constructor() {
+ super("passwords", "id", [
+ "hostname",
+ "formSubmitURL",
+ "httpRealm",
+ "password",
+ "passwordField",
+ "username",
+ "usernameField",
+ ]);
+ }
+
+ getClientItems() {
+ let logins = Services.logins.getAllLogins({});
+ let syncHosts = Utils.getSyncCredentialsHosts();
+ let result = logins.map(l => l.QueryInterface(Ci.nsILoginMetaInfo))
+ .filter(l => !syncHosts.has(l.hostname));
+ return Promise.resolve(result);
+ }
+
+ normalizeClientItem(item) {
+ return {
+ id: item.guid,
+ guid: item.guid,
+ hostname: item.hostname,
+ formSubmitURL: item.formSubmitURL,
+ httpRealm: item.httpRealm,
+ password: item.password,
+ passwordField: item.passwordField,
+ username: item.username,
+ usernameField: item.usernameField,
+ original: item,
+ };
+ }
+
+ async normalizeServerItem(item) {
+ return Object.assign({ guid: item.id }, item);
+ }
+}
+
+
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/repair_manager.jsm
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["RepairManager"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/Log.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Weave", "resource://services-sync/main.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Doctor", "resource://sync-repair/doctor.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "getRepairRequestor",
+ "resource://sync-repair/collection_repair.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "getRepairResponder",
+ "resource://sync-repair/collection_repair.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ return Log.repository.getLogger("Sync.Repair.Manager");
+});
+
+class RepairManager {
+ constructor() {
+ log.info("Repair manager initialized");
+ }
+
+ async consultDoctor(engines) {
+ return Doctor.consult(engines);
+ }
+
+ async handleRepairReponse(response, rawCommand) {
+ let requestor = getRepairRequestor(response.collection);
+ if (!requestor) {
+ log.warn("repairResponse for unknown collection", response);
+ return;
+ }
+ if (!(await requestor.continueRepairs(response))) {
+ log.warn("repairResponse couldn't continue the repair", response);
+ }
+ }
+
+ async handleRepairRequest(request, rawCommand) {
+ // Another device has sent us a request to make some repair.
+ let responder = getRepairResponder(request.collection);
+ if (!responder) {
+ log.warn("repairRequest for unknown collection", request);
+ return false;
+ }
+ return responder.repair(request, rawCommand);
+ }
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "plugin:mozilla/xpcshell-test",
+ ],
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/head.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../../../../services/sync/tests/unit/head_appinfo.js */
+/* import-globals-from ../../../../../services/common/tests/unit/head_helpers.js */
+/* import-globals-from ../../../../../services/sync/tests/unit/head_helpers.js */
+/* import-globals-from ../../../../../services/sync/tests/unit/head_http_server.js */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+(function() {
+ // Load our bootstrap extension manifest so we can access our chrome/resource URIs.
+ const EXTENSION_ID = "sync-repair@mozilla.org";
+ let extensionDir = Services.dirsvc.get("GreD", Components.interfaces.nsIFile);
+ extensionDir.append("browser");
+ extensionDir.append("features");
+ extensionDir.append(EXTENSION_ID);
+ Components.manager.addBootstrappedManifestLocation(extensionDir);
+}());
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/test_bookmark_repair.js
@@ -0,0 +1,522 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from head.js */
+
+
+// Tests the bookmark repair requestor and responder end-to-end (ie, without
+// many mocks)
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/engines/clients.js");
+Cu.import("resource://services-sync/engines/bookmarks.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+Cu.import("resource://sync-repair/repair_manager.jsm");
+Cu.import("resource://sync-repair/bookmark_repair.jsm");
+Cu.import("resource://sync-repair/doctor.jsm");
+
+const LAST_BOOKMARK_SYNC_PREFS = [
+ "bookmarks.lastSync",
+ "bookmarks.lastSyncLocal",
+];
+
+const BOOKMARK_REPAIR_STATE_PREFS = [
+ "client.GUID",
+ "doctor.lastRepairAdvance",
+ ...LAST_BOOKMARK_SYNC_PREFS,
+ ...Object.values(BookmarkRepairRequestor.PREF).map(name =>
+ `repairs.bookmarks.${name}`
+ ),
+];
+
+let clientsEngine;
+let bookmarksEngine;
+var recordedEvents = [];
+
+add_task(async function setup() {
+ clientsEngine = Service.clientsEngine;
+ clientsEngine.ignoreLastModifiedOnProcessCommands = true;
+ bookmarksEngine = Service.engineManager.get("bookmarks");
+ // Service.setRepairManager(new RepairManager());
+
+ await generateNewKeys(Service.collectionKeys);
+
+ Service.recordTelemetryEvent = (object, method, value, extra = undefined) => {
+ recordedEvents.push({ object, method, value, extra });
+ };
+
+ initTestLogging("Trace");
+ Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
+ Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace;
+ Log.repository.getLogger("Sqlite").level = Log.Level.Info; // less noisy
+});
+
+function checkRecordedEvents(expected, message) {
+ deepEqual(recordedEvents, expected, message);
+ // and clear the list so future checks are easier to write.
+ recordedEvents = [];
+}
+
+// Backs up and resets all preferences to their default values. Returns a
+// function that restores the preferences when called.
+function backupPrefs(names) {
+ let state = new Map();
+ for (let name of names) {
+ state.set(name, Svc.Prefs.get(name));
+ Svc.Prefs.reset(name);
+ }
+ return () => {
+ for (let [name, value] of state) {
+ Svc.Prefs.set(name, value);
+ }
+ };
+}
+
+async function promiseValidationDone(expected) {
+ // wait for a validation to complete.
+ let obs = promiseOneObserver("weave:engine:validate:finish");
+ let { subject: validationResult } = await obs;
+ // check the results - anything non-zero is checked against |expected|
+ let summary = validationResult.problems.getSummary();
+ let actual = summary.filter(({name, count}) => count);
+ actual.sort((a, b) => String(a.name).localeCompare(b.name));
+ expected.sort((a, b) => String(a.name).localeCompare(b.name));
+ deepEqual(actual, expected);
+}
+
+async function cleanup(server) {
+ await bookmarksEngine._store.wipe();
+ await clientsEngine._store.wipe();
+ Svc.Prefs.resetBranch("");
+ Service.recordManager.clearCache();
+ await promiseStopServer(server);
+}
+
+add_task(async function test_bookmark_repair_integration() {
+ forceBookmarkValidation();
+
+ _("Ensure that a validation error triggers a repair request.");
+
+ let server = await serverForFoo(bookmarksEngine);
+ await SyncTestingInfrastructure(server);
+
+ let user = server.user("foo");
+
+ let initialID = Service.clientsEngine.localID;
+ let remoteID = Utils.makeGUID();
+ try {
+
+ _("Syncing to initialize crypto etc.");
+ await Service.sync();
+
+ _("Create remote client record");
+ server.insertWBO("foo", "clients", new ServerWBO(remoteID, encryptPayload({
+ id: remoteID,
+ name: "Remote client",
+ type: "desktop",
+ commands: [],
+ version: "54",
+ protocols: ["1.5"],
+ }), Date.now() / 1000));
+
+ _("Create bookmark and folder");
+ let folderInfo = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "Folder 1",
+ });
+ let bookmarkInfo = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderInfo.guid,
+ url: "http://getfirefox.com/",
+ title: "Get Firefox!",
+ });
+
+ _(`Upload ${folderInfo.guid} and ${bookmarkInfo.guid} to server`);
+ let validationPromise = promiseValidationDone([]);
+ await Service.sync();
+ equal(clientsEngine.stats.numClients, 2, "Clients collection should have 2 records");
+ await validationPromise;
+ checkRecordedEvents([], "Should not start repair after first sync");
+
+ _("Back up last sync timestamps for remote client");
+ let restoreRemoteLastBookmarkSync = backupPrefs(LAST_BOOKMARK_SYNC_PREFS);
+
+ _(`Delete ${bookmarkInfo.guid} locally and on server`);
+ // Now we will reach into the server and hard-delete the bookmark
+ user.collection("bookmarks").remove(bookmarkInfo.guid);
+ // And delete the bookmark, but cheat by telling places that Sync did
+ // it, so we don't end up with a tombstone.
+ await PlacesUtils.bookmarks.remove(bookmarkInfo.guid, {
+ source: PlacesUtils.bookmarks.SOURCE_SYNC,
+ });
+ deepEqual((await bookmarksEngine.pullNewChanges()), {},
+ `Should not upload tombstone for ${bookmarkInfo.guid}`);
+
+ // sync again - we should have a few problems...
+ _("Sync again to trigger repair");
+ validationPromise = promiseValidationDone([
+ {"name": "missingChildren", "count": 1},
+ {"name": "structuralDifferences", "count": 1},
+ ]);
+ await Service.sync();
+ await validationPromise;
+ let flowID = Svc.Prefs.get("repairs.bookmarks.flowID");
+ checkRecordedEvents([{
+ object: "repair",
+ method: "started",
+ value: undefined,
+ extra: {
+ flowID,
+ numIDs: "2",
+ },
+ }, {
+ object: "sendcommand",
+ method: "repairRequest",
+ value: undefined,
+ extra: {
+ flowID,
+ deviceID: Service.identity.hashedDeviceID(remoteID),
+ },
+ }, {
+ object: "repair",
+ method: "request",
+ value: "upload",
+ extra: {
+ deviceID: Service.identity.hashedDeviceID(remoteID),
+ flowID,
+ numIDs: "2",
+ },
+ }], "Should record telemetry events for repair request");
+
+ // We should have started a repair with our second client.
+ equal((await clientsEngine.getClientCommands(remoteID)).length, 1,
+ "Should queue repair request for remote client after repair");
+ _("Sync to send outgoing repair request");
+ await Service.sync();
+ equal((await clientsEngine.getClientCommands(remoteID)).length, 0,
+ "Should send repair request to remote client after next sync");
+ checkRecordedEvents([],
+ "Should not record repair telemetry after sending repair request");
+
+ _("Back up repair state to restore later");
+ let restoreInitialRepairState = backupPrefs(BOOKMARK_REPAIR_STATE_PREFS);
+
+ // so now let's take over the role of that other client!
+ _("Create new clients engine pretending to be remote client");
+ let remoteClientsEngine = Service.clientsEngine = new ClientEngine(Service);
+ remoteClientsEngine.ignoreLastModifiedOnProcessCommands = true;
+ await remoteClientsEngine.initialize();
+ remoteClientsEngine.localID = remoteID;
+
+ _("Restore missing bookmark");
+ // Pretend Sync wrote the bookmark, so that we upload it as part of the
+ // repair instead of the sync.
+ bookmarkInfo.source = PlacesUtils.bookmarks.SOURCE_SYNC;
+ await PlacesUtils.bookmarks.insert(bookmarkInfo);
+ restoreRemoteLastBookmarkSync();
+
+ _("Sync as remote client");
+ await Service.sync();
+ checkRecordedEvents([{
+ object: "processcommand",
+ method: "repairRequest",
+ value: undefined,
+ extra: {
+ flowID,
+ },
+ }, {
+ object: "repairResponse",
+ method: "uploading",
+ value: undefined,
+ extra: {
+ flowID,
+ numIDs: "2",
+ },
+ }, {
+ object: "sendcommand",
+ method: "repairResponse",
+ value: undefined,
+ extra: {
+ flowID,
+ deviceID: Service.identity.hashedDeviceID(initialID),
+ },
+ }, {
+ object: "repairResponse",
+ method: "finished",
+ value: undefined,
+ extra: {
+ flowID,
+ numIDs: "2",
+ }
+ }], "Should record telemetry events for repair response");
+
+ // We should queue the repair response for the initial client.
+ equal((await remoteClientsEngine.getClientCommands(initialID)).length, 1,
+ "Should queue repair response for initial client after repair");
+ ok(user.collection("bookmarks").wbo(bookmarkInfo.guid),
+ "Should upload missing bookmark");
+
+ _("Sync to upload bookmark and send outgoing repair response");
+ await Service.sync();
+ equal((await remoteClientsEngine.getClientCommands(initialID)).length, 0,
+ "Should send repair response to initial client after next sync");
+ checkRecordedEvents([],
+ "Should not record repair telemetry after sending repair response");
+ ok(!Services.prefs.prefHasUserValue("services.sync.repairs.bookmarks.state"),
+ "Remote client should not be repairing");
+
+ _("Pretend to be initial client again");
+ Service.clientsEngine = clientsEngine;
+
+ _("Restore incomplete Places database and prefs");
+ await PlacesUtils.bookmarks.remove(bookmarkInfo.guid, {
+ source: PlacesUtils.bookmarks.SOURCE_SYNC,
+ });
+ restoreInitialRepairState();
+ ok(Services.prefs.prefHasUserValue("services.sync.repairs.bookmarks.state"),
+ "Initial client should still be repairing");
+
+ _("Sync as initial client");
+ let revalidationPromise = promiseValidationDone([]);
+ await Service.sync();
+ let restoredBookmarkInfo = await PlacesUtils.bookmarks.fetch(bookmarkInfo.guid);
+ ok(restoredBookmarkInfo, "Missing bookmark should be downloaded to initial client");
+ checkRecordedEvents([{
+ object: "processcommand",
+ method: "repairResponse",
+ value: undefined,
+ extra: {
+ flowID,
+ },
+ }, {
+ object: "repair",
+ method: "response",
+ value: "upload",
+ extra: {
+ flowID,
+ deviceID: Service.identity.hashedDeviceID(remoteID),
+ numIDs: "2",
+ },
+ }, {
+ object: "repair",
+ method: "finished",
+ value: undefined,
+ extra: {
+ flowID,
+ numIDs: "0",
+ },
+ }]);
+ await revalidationPromise;
+ ok(!Services.prefs.prefHasUserValue("services.sync.repairs.bookmarks.state"),
+ "Should clear repair pref after successfully completing repair");
+ } finally {
+ await cleanup(server);
+ clientsEngine = Service.clientsEngine = new ClientEngine(Service);
+ clientsEngine.ignoreLastModifiedOnProcessCommands = true;
+ clientsEngine.initialize();
+ }
+});
+
+add_task(async function test_repair_client_missing() {
+ forceBookmarkValidation();
+
+ _("Ensure that a record missing from the client only will get re-downloaded from the server");
+
+ let server = await serverForFoo(bookmarksEngine);
+ await SyncTestingInfrastructure(server);
+
+ let remoteID = Utils.makeGUID();
+ try {
+
+ _("Syncing to initialize crypto etc.");
+ await Service.sync();
+
+ _("Create remote client record");
+ server.insertWBO("foo", "clients", new ServerWBO(remoteID, encryptPayload({
+ id: remoteID,
+ name: "Remote client",
+ type: "desktop",
+ commands: [],
+ version: "54",
+ protocols: ["1.5"],
+ }), Date.now() / 1000));
+
+ let bookmarkInfo = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://getfirefox.com/",
+ title: "Get Firefox!",
+ });
+
+ let validationPromise = promiseValidationDone([]);
+ _("Syncing.");
+ await Service.sync();
+ // should have 2 clients
+ equal(clientsEngine.stats.numClients, 2);
+ await validationPromise;
+
+ // Delete the bookmark localy, but cheat by telling places that Sync did
+ // it, so Sync still thinks we have it.
+ await PlacesUtils.bookmarks.remove(bookmarkInfo.guid, {
+ source: PlacesUtils.bookmarks.SOURCE_SYNC,
+ });
+ // sanity check we aren't going to sync this removal.
+ do_check_empty((await bookmarksEngine.pullNewChanges()));
+ // sanity check that the bookmark is not there anymore
+ do_check_false(await PlacesUtils.bookmarks.fetch(bookmarkInfo.guid));
+
+ // sync again - we should have a few problems...
+ _("Syncing again.");
+ validationPromise = promiseValidationDone([
+ {"name": "clientMissing", "count": 1},
+ {"name": "structuralDifferences", "count": 1},
+ ]);
+ await Service.sync();
+ await validationPromise;
+
+ // We shouldn't have started a repair with our second client.
+ equal((await clientsEngine.getClientCommands(remoteID)).length, 0);
+
+ // Trigger a sync (will request the missing item)
+ await Service.sync();
+
+ // And we got our bookmark back
+ do_check_true(await PlacesUtils.bookmarks.fetch(bookmarkInfo.guid));
+ } finally {
+ await cleanup(server);
+ }
+});
+
+add_task(async function test_repair_server_missing() {
+ forceBookmarkValidation();
+
+ _("Ensure that a record missing from the server only will get re-upload from the client");
+
+ let server = await serverForFoo(bookmarksEngine);
+ await SyncTestingInfrastructure(server);
+
+ let user = server.user("foo");
+
+ let remoteID = Utils.makeGUID();
+ try {
+
+ _("Syncing to initialize crypto etc.");
+ await Service.sync();
+
+ _("Create remote client record");
+ server.insertWBO("foo", "clients", new ServerWBO(remoteID, encryptPayload({
+ id: remoteID,
+ name: "Remote client",
+ type: "desktop",
+ commands: [],
+ version: "54",
+ protocols: ["1.5"],
+ }), Date.now() / 1000));
+
+ let bookmarkInfo = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://getfirefox.com/",
+ title: "Get Firefox!",
+ });
+
+ let validationPromise = promiseValidationDone([]);
+ _("Syncing.");
+ await Service.sync();
+ // should have 2 clients
+ equal(clientsEngine.stats.numClients, 2);
+ await validationPromise;
+
+ // Now we will reach into the server and hard-delete the bookmark
+ user.collection("bookmarks").wbo(bookmarkInfo.guid).delete();
+
+ // sync again - we should have a few problems...
+ _("Syncing again.");
+ validationPromise = promiseValidationDone([
+ {"name": "serverMissing", "count": 1},
+ {"name": "missingChildren", "count": 1},
+ ]);
+ await Service.sync();
+ await validationPromise;
+
+ // We shouldn't have started a repair with our second client.
+ equal((await clientsEngine.getClientCommands(remoteID)).length, 0);
+
+ // Trigger a sync (will upload the missing item)
+ await Service.sync();
+
+ // And the server got our bookmark back
+ do_check_true(user.collection("bookmarks").wbo(bookmarkInfo.guid));
+ } finally {
+ await cleanup(server);
+ }
+});
+
+add_task(async function test_repair_server_deleted() {
+ forceBookmarkValidation();
+
+ _("Ensure that a record marked as deleted on the server but present on the client will get deleted on the client");
+
+ let server = await serverForFoo(bookmarksEngine);
+ await SyncTestingInfrastructure(server);
+
+ let remoteID = Utils.makeGUID();
+ try {
+
+ _("Syncing to initialize crypto etc.");
+ await Service.sync();
+
+ _("Create remote client record");
+ server.insertWBO("foo", "clients", new ServerWBO(remoteID, encryptPayload({
+ id: remoteID,
+ name: "Remote client",
+ type: "desktop",
+ commands: [],
+ version: "54",
+ protocols: ["1.5"],
+ }), Date.now() / 1000));
+
+ let bookmarkInfo = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://getfirefox.com/",
+ title: "Get Firefox!",
+ });
+
+ let validationPromise = promiseValidationDone([]);
+ _("Syncing.");
+ await Service.sync();
+ // should have 2 clients
+ equal(clientsEngine.stats.numClients, 2);
+ await validationPromise;
+
+ // Now we will reach into the server and create a tombstone for that bookmark
+ // but with a last-modified in the past - this way our sync isn't going to
+ // pick up the record.
+ server.insertWBO("foo", "bookmarks", new ServerWBO(bookmarkInfo.guid, encryptPayload({
+ id: bookmarkInfo.guid,
+ deleted: true,
+ }), (Date.now() - 60000) / 1000));
+
+ // sync again - we should have a few problems...
+ _("Syncing again.");
+ validationPromise = promiseValidationDone([
+ {"name": "serverDeleted", "count": 1},
+ {"name": "deletedChildren", "count": 1},
+ {"name": "orphans", "count": 1}
+ ]);
+ await Service.sync();
+ await validationPromise;
+
+ // We shouldn't have started a repair with our second client.
+ equal((await clientsEngine.getClientCommands(remoteID)).length, 0);
+
+ // Trigger a sync (will upload the missing item)
+ await Service.sync();
+
+ // And the client deleted our bookmark
+ do_check_true(!(await PlacesUtils.bookmarks.fetch(bookmarkInfo.guid)));
+ } finally {
+ await cleanup(server);
+ }
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/test_bookmark_repair_requestor.js
@@ -0,0 +1,514 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+Cu.import("resource://sync-repair/bookmark_repair.jsm");
+
+initTestLogging("Trace");
+
+function makeClientRecord(id, fields = {}) {
+ return {
+ id,
+ version: fields.version || "54.0a1",
+ type: fields.type || "desktop",
+ stale: fields.stale || false,
+ serverLastModified: fields.serverLastModified || 0,
+ };
+}
+
+class MockClientsEngine {
+ constructor(clientList) {
+ this._clientList = clientList;
+ this._sentCommands = {};
+ }
+
+ get remoteClients() {
+ return Object.values(this._clientList);
+ }
+
+ remoteClient(id) {
+ return this._clientList[id];
+ }
+
+ async sendCommand(command, args, clientID) {
+ let cc = this._sentCommands[clientID] || [];
+ cc.push({ command, args });
+ this._sentCommands[clientID] = cc;
+ }
+
+ async getClientCommands(clientID) {
+ return this._sentCommands[clientID] || [];
+ }
+}
+
+class MockIdentity {
+ hashedDeviceID(did) {
+ return did; // don't hash it to make testing easier.
+ }
+}
+
+class MockService {
+ constructor(clientList) {
+ this.clientsEngine = new MockClientsEngine(clientList);
+ this.identity = new MockIdentity();
+ this._recordedEvents = [];
+ }
+
+ recordTelemetryEvent(object, method, value, extra = undefined) {
+ this._recordedEvents.push({ method, object, value, extra });
+ }
+}
+
+function checkState(expected) {
+ equal(Services.prefs.getCharPref("services.sync.repairs.bookmarks.state"), expected);
+}
+
+function checkRepairFinished() {
+ try {
+ let state = Services.prefs.getCharPref("services.sync.repairs.bookmarks.state");
+ ok(false, state);
+ } catch (ex) {
+ ok(true, "no repair preference exists");
+ }
+}
+
+function checkOutgoingCommand(service, clientID) {
+ let sent = service.clientsEngine._sentCommands;
+ deepEqual(Object.keys(sent), [clientID]);
+ equal(sent[clientID].length, 1);
+ equal(sent[clientID][0].command, "repairRequest");
+}
+
+function NewBookmarkRepairRequestor(mockService) {
+ let req = new BookmarkRepairRequestor(mockService);
+ req._now = () => Date.now() / 1000; // _now() is seconds.
+ return req;
+}
+
+add_task(async function test_requestor_no_clients() {
+ let mockService = new MockService({ });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ let validationInfo = {
+ problems: {
+ missingChildren: [
+ {parent: "x", child: "a"},
+ {parent: "x", child: "b"},
+ {parent: "x", child: "c"}
+ ],
+ orphans: [],
+ }
+ };
+ let flowID = Utils.makeGUID();
+
+ await requestor.startRepairs(validationInfo, flowID);
+ // there are no clients, so we should end up in "finished" (which we need to
+ // check via telemetry)
+ deepEqual(mockService._recordedEvents, [
+ { object: "repair",
+ method: "started",
+ value: undefined,
+ extra: { flowID, numIDs: 4 },
+ },
+ { object: "repair",
+ method: "finished",
+ value: undefined,
+ extra: { flowID, numIDs: 4 },
+ }
+ ]);
+});
+
+add_task(async function test_requestor_one_client_no_response() {
+ let mockService = new MockService({ "client-a": makeClientRecord("client-a") });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ let validationInfo = {
+ problems: {
+ missingChildren: [
+ {parent: "x", child: "a"},
+ {parent: "x", child: "b"},
+ {parent: "x", child: "c"}
+ ],
+ orphans: [],
+ }
+ };
+ let flowID = Utils.makeGUID();
+ await requestor.startRepairs(validationInfo, flowID);
+ // the command should now be outgoing.
+ checkOutgoingCommand(mockService, "client-a");
+
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+ // asking it to continue stays in that state until we timeout or the command
+ // is removed.
+ await requestor.continueRepairs();
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+
+ // now pretend that client synced.
+ mockService.clientsEngine._sentCommands = {};
+ await requestor.continueRepairs();
+ checkState(BookmarkRepairRequestor.STATE.SENT_SECOND_REQUEST);
+ // the command should be outgoing again.
+ checkOutgoingCommand(mockService, "client-a");
+
+ // pretend that client synced again without writing a command.
+ mockService.clientsEngine._sentCommands = {};
+ await requestor.continueRepairs();
+ // There are no more clients, so we've given up.
+
+ checkRepairFinished();
+ deepEqual(mockService._recordedEvents, [
+ { object: "repair",
+ method: "started",
+ value: undefined,
+ extra: { flowID, numIDs: 4 },
+ },
+ { object: "repair",
+ method: "request",
+ value: "upload",
+ extra: { flowID, numIDs: 4, deviceID: "client-a" },
+ },
+ { object: "repair",
+ method: "request",
+ value: "upload",
+ extra: { flowID, numIDs: 4, deviceID: "client-a" },
+ },
+ { object: "repair",
+ method: "finished",
+ value: undefined,
+ extra: { flowID, numIDs: 4 },
+ }
+ ]);
+});
+
+add_task(async function test_requestor_one_client_no_sync() {
+ let mockService = new MockService({ "client-a": makeClientRecord("client-a") });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ let validationInfo = {
+ problems: {
+ missingChildren: [
+ {parent: "x", child: "a"},
+ {parent: "x", child: "b"},
+ {parent: "x", child: "c"}
+ ],
+ orphans: [],
+ }
+ };
+ let flowID = Utils.makeGUID();
+ await requestor.startRepairs(validationInfo, flowID);
+ // the command should now be outgoing.
+ checkOutgoingCommand(mockService, "client-a");
+
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+
+ // pretend we are now in the future.
+ let theFuture = Date.now() + 300000000;
+ requestor._now = () => theFuture;
+
+ await requestor.continueRepairs();
+
+ // We should be finished as we gave up in disgust.
+ checkRepairFinished();
+ deepEqual(mockService._recordedEvents, [
+ { object: "repair",
+ method: "started",
+ value: undefined,
+ extra: { flowID, numIDs: 4 },
+ },
+ { object: "repair",
+ method: "request",
+ value: "upload",
+ extra: { flowID, numIDs: 4, deviceID: "client-a" },
+ },
+ { object: "repair",
+ method: "abandon",
+ value: "silent",
+ extra: { flowID, deviceID: "client-a" },
+ },
+ { object: "repair",
+ method: "finished",
+ value: undefined,
+ extra: { flowID, numIDs: 4 },
+ }
+ ]);
+});
+
+add_task(async function test_requestor_latest_client_used() {
+ let mockService = new MockService({
+ "client-early": makeClientRecord("client-early", { serverLastModified: Date.now() - 10 }),
+ "client-late": makeClientRecord("client-late", { serverLastModified: Date.now() }),
+ });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ let validationInfo = {
+ problems: {
+ missingChildren: [
+ { parent: "x", child: "a" },
+ ],
+ orphans: [],
+ }
+ };
+ await requestor.startRepairs(validationInfo, Utils.makeGUID());
+ // the repair command should be outgoing to the most-recent client.
+ checkOutgoingCommand(mockService, "client-late");
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+ // and this test is done - reset the repair.
+ requestor.prefs.resetBranch();
+});
+
+add_task(async function test_requestor_client_vanishes() {
+ let mockService = new MockService({
+ "client-a": makeClientRecord("client-a"),
+ "client-b": makeClientRecord("client-b"),
+ });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ let validationInfo = {
+ problems: {
+ missingChildren: [
+ {parent: "x", child: "a"},
+ {parent: "x", child: "b"},
+ {parent: "x", child: "c"}
+ ],
+ orphans: [],
+ }
+ };
+ let flowID = Utils.makeGUID();
+ await requestor.startRepairs(validationInfo, flowID);
+ // the command should now be outgoing.
+ checkOutgoingCommand(mockService, "client-a");
+
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+
+ mockService.clientsEngine._sentCommands = {};
+ // Now let's pretend the client vanished.
+ delete mockService.clientsEngine._clientList["client-a"];
+
+ await requestor.continueRepairs();
+ // We should have moved on to client-b.
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+ checkOutgoingCommand(mockService, "client-b");
+
+ // Now let's pretend client B wrote all missing IDs.
+ let response = {
+ collection: "bookmarks",
+ request: "upload",
+ flowID: requestor._flowID,
+ clientID: "client-b",
+ ids: ["a", "b", "c", "x"],
+ };
+ await requestor.continueRepairs(response);
+
+ // We should be finished as we got all our IDs.
+ checkRepairFinished();
+ deepEqual(mockService._recordedEvents, [
+ { object: "repair",
+ method: "started",
+ value: undefined,
+ extra: { flowID, numIDs: 4 },
+ },
+ { object: "repair",
+ method: "request",
+ value: "upload",
+ extra: { flowID, numIDs: 4, deviceID: "client-a" },
+ },
+ { object: "repair",
+ method: "abandon",
+ value: "missing",
+ extra: { flowID, deviceID: "client-a" },
+ },
+ { object: "repair",
+ method: "request",
+ value: "upload",
+ extra: { flowID, numIDs: 4, deviceID: "client-b" },
+ },
+ { object: "repair",
+ method: "response",
+ value: "upload",
+ extra: { flowID, deviceID: "client-b", numIDs: 4 },
+ },
+ { object: "repair",
+ method: "finished",
+ value: undefined,
+ extra: { flowID, numIDs: 0 },
+ }
+ ]);
+});
+
+add_task(async function test_requestor_success_responses() {
+ let mockService = new MockService({
+ "client-a": makeClientRecord("client-a"),
+ "client-b": makeClientRecord("client-b"),
+ });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ let validationInfo = {
+ problems: {
+ missingChildren: [
+ {parent: "x", child: "a"},
+ {parent: "x", child: "b"},
+ {parent: "x", child: "c"}
+ ],
+ orphans: [],
+ }
+ };
+ let flowID = Utils.makeGUID();
+ await requestor.startRepairs(validationInfo, flowID);
+ // the command should now be outgoing.
+ checkOutgoingCommand(mockService, "client-a");
+
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+
+ mockService.clientsEngine._sentCommands = {};
+ // Now let's pretend the client wrote a response.
+ let response = {
+ collection: "bookmarks",
+ request: "upload",
+ clientID: "client-a",
+ flowID: requestor._flowID,
+ ids: ["a", "b"],
+ };
+ await requestor.continueRepairs(response);
+ // We should have moved on to client 2.
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+ checkOutgoingCommand(mockService, "client-b");
+
+ // Now let's pretend client B write the missing ID.
+ response = {
+ collection: "bookmarks",
+ request: "upload",
+ clientID: "client-b",
+ flowID: requestor._flowID,
+ ids: ["c", "x"],
+ };
+ await requestor.continueRepairs(response);
+
+ // We should be finished as we got all our IDs.
+ checkRepairFinished();
+ deepEqual(mockService._recordedEvents, [
+ { object: "repair",
+ method: "started",
+ value: undefined,
+ extra: { flowID, numIDs: 4 },
+ },
+ { object: "repair",
+ method: "request",
+ value: "upload",
+ extra: { flowID, numIDs: 4, deviceID: "client-a" },
+ },
+ { object: "repair",
+ method: "response",
+ value: "upload",
+ extra: { flowID, deviceID: "client-a", numIDs: 2 },
+ },
+ { object: "repair",
+ method: "request",
+ value: "upload",
+ extra: { flowID, numIDs: 2, deviceID: "client-b" },
+ },
+ { object: "repair",
+ method: "response",
+ value: "upload",
+ extra: { flowID, deviceID: "client-b", numIDs: 2 },
+ },
+ { object: "repair",
+ method: "finished",
+ value: undefined,
+ extra: { flowID, numIDs: 0 },
+ }
+ ]);
+});
+
+add_task(async function test_client_suitability() {
+ let mockService = new MockService({
+ "client-a": makeClientRecord("client-a"),
+ "client-b": makeClientRecord("client-b", { type: "mobile" }),
+ "client-c": makeClientRecord("client-c", { version: "52.0a1" }),
+ "client-d": makeClientRecord("client-c", { version: "54.0a1" }),
+ });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ ok(requestor._isSuitableClient(mockService.clientsEngine.remoteClient("client-a")));
+ ok(!requestor._isSuitableClient(mockService.clientsEngine.remoteClient("client-b")));
+ ok(!requestor._isSuitableClient(mockService.clientsEngine.remoteClient("client-c")));
+ ok(requestor._isSuitableClient(mockService.clientsEngine.remoteClient("client-d")));
+});
+
+add_task(async function test_requestor_already_repairing_at_start() {
+ let mockService = new MockService({ });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ requestor.anyClientsRepairing = () => true;
+ let validationInfo = {
+ problems: {
+ missingChildren: [
+ {parent: "x", child: "a"},
+ {parent: "x", child: "b"},
+ {parent: "x", child: "c"}
+ ],
+ orphans: [],
+ }
+ };
+ let flowID = Utils.makeGUID();
+
+ ok(!(await requestor.startRepairs(validationInfo, flowID)),
+ "Shouldn't start repairs");
+ equal(mockService._recordedEvents.length, 1);
+ equal(mockService._recordedEvents[0].method, "aborted");
+});
+
+add_task(async function test_requestor_already_repairing_continue() {
+ let clientB = makeClientRecord("client-b");
+ let mockService = new MockService({
+ "client-a": makeClientRecord("client-a"),
+ "client-b": clientB
+ });
+ let requestor = NewBookmarkRepairRequestor(mockService);
+ let validationInfo = {
+ problems: {
+ missingChildren: [
+ {parent: "x", child: "a"},
+ {parent: "x", child: "b"},
+ {parent: "x", child: "c"}
+ ],
+ orphans: [],
+ }
+ };
+ let flowID = Utils.makeGUID();
+ await requestor.startRepairs(validationInfo, flowID);
+ // the command should now be outgoing.
+ checkOutgoingCommand(mockService, "client-a");
+
+ checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
+ mockService.clientsEngine._sentCommands = {};
+
+ // Now let's pretend the client wrote a response (it doesn't matter what's in here)
+ let response = {
+ collection: "bookmarks",
+ request: "upload",
+ clientID: "client-a",
+ flowID: requestor._flowID,
+ ids: ["a", "b"],
+ };
+
+ // and another client also started a request
+ clientB.commands = [{
+ args: [{ collection: "bookmarks", flowID: "asdf" }],
+ command: "repairRequest",
+ }];
+
+
+ await requestor.continueRepairs(response);
+
+ // We should have aborted now
+ checkRepairFinished();
+ const expected = [
+ { method: "started",
+ object: "repair",
+ value: undefined,
+ extra: { flowID, numIDs: "4" },
+ },
+ { method: "request",
+ object: "repair",
+ value: "upload",
+ extra: { flowID, numIDs: "4", deviceID: "client-a" },
+ },
+ { method: "aborted",
+ object: "repair",
+ value: undefined,
+ extra: { flowID, numIDs: "4", reason: "other clients repairing" },
+ }
+ ];
+
+ deepEqual(mockService._recordedEvents, expected);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/test_bookmark_repair_responder.js
@@ -0,0 +1,611 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource:///modules/PlacesUIUtils.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/engines/bookmarks.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://sync-repair/bookmark_repair.jsm");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+initTestLogging("Trace");
+Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
+
+// Disable validation so that we don't try to automatically repair the server
+// when we sync.
+Svc.Prefs.set("engine.bookmarks.validation.enabled", false);
+
+// stub telemetry so we can easily check the right things are recorded.
+var recordedEvents = [];
+
+function checkRecordedEvents(expected) {
+ deepEqual(recordedEvents, expected);
+ // and clear the list so future checks are easier to write.
+ recordedEvents = [];
+}
+
+function getServerBookmarks(server) {
+ return server.user("foo").collection("bookmarks");
+}
+
+async function makeServer() {
+ let server = await serverForFoo(bookmarksEngine);
+ await SyncTestingInfrastructure(server);
+ return server;
+}
+
+async function cleanup(server) {
+ await promiseStopServer(server);
+ await PlacesSyncUtils.bookmarks.wipe();
+ // clear keys so when each test finds a different server it accepts its keys.
+ Service.collectionKeys.clear();
+}
+
+let bookmarksEngine;
+
+add_task(async function setup() {
+ bookmarksEngine = Service.engineManager.get("bookmarks");
+
+ Service.recordTelemetryEvent = (object, method, value, extra = undefined) => {
+ recordedEvents.push({ object, method, value, extra });
+ };
+});
+
+add_task(async function test_responder_error() {
+ let server = await makeServer();
+
+ // sync so the collection is created.
+ await Service.sync();
+
+ let request = {
+ request: "upload",
+ ids: [Utils.makeGUID()],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ // mock the responder to simulate an error.
+ responder._fetchItemsToUpload = async function() {
+ throw new Error("oh no!");
+ };
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "failed",
+ value: undefined,
+ extra: { flowID: request.flowID,
+ numIDs: "0",
+ failureReason: '{"name":"unexpectederror","error":"Error: oh no!"}',
+ }
+ },
+ ]);
+
+ await cleanup(server);
+});
+
+add_task(async function test_responder_no_items() {
+ let server = await makeServer();
+
+ // sync so the collection is created.
+ await Service.sync();
+
+ let request = {
+ request: "upload",
+ ids: [Utils.makeGUID()],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "finished",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "0"},
+ },
+ ]);
+
+ await cleanup(server);
+});
+
+// One item requested and we have it locally, but it's not yet on the server.
+add_task(async function test_responder_upload() {
+ let server = await makeServer();
+
+ // Pretend we've already synced this bookmark, so that we can ensure it's
+ // uploaded in response to our repair request.
+ let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Get Firefox",
+ url: "http://getfirefox.com/",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC });
+
+ await Service.sync();
+ deepEqual(getServerBookmarks(server).keys().sort(), [
+ "menu",
+ "mobile",
+ "toolbar",
+ "unfiled",
+ ], "Should only upload roots on first sync");
+
+ let request = {
+ request: "upload",
+ ids: [bm.guid],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "uploading",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "1"},
+ },
+ ]);
+
+ await Service.sync();
+ deepEqual(getServerBookmarks(server).keys().sort(), [
+ "menu",
+ "mobile",
+ "toolbar",
+ "unfiled",
+ bm.guid,
+ ].sort(), "Should upload requested bookmark on second sync");
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "finished",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "1"},
+ },
+ ]);
+
+ await cleanup(server);
+});
+
+// One item requested and we have it locally and it's already on the server.
+// As it was explicitly requested, we should upload it.
+add_task(async function test_responder_item_exists_locally() {
+ let server = await makeServer();
+
+ let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Get Firefox",
+ url: "http://getfirefox.com/" });
+ // first sync to get the item on the server.
+ _("Syncing to get item on the server");
+ await Service.sync();
+
+ // issue a repair request for it.
+ let request = {
+ request: "upload",
+ ids: [bm.guid],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ await responder.repair(request, null);
+
+ // We still re-upload the item.
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "uploading",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "1"},
+ },
+ ]);
+
+ _("Syncing to do the upload.");
+ await Service.sync();
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "finished",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "1"},
+ },
+ ]);
+ await cleanup(server);
+});
+
+add_task(async function test_responder_tombstone() {
+ let server = await makeServer();
+
+ // TODO: Request an item for which we have a tombstone locally. Decide if
+ // we want to store tombstones permanently for this. In the integration
+ // test, we can also try requesting a deleted child or ancestor.
+
+ // For now, we'll handle this identically to `test_responder_missing_items`.
+ // Bug 1343103 is a follow-up to better handle this.
+ await cleanup(server);
+});
+
+add_task(async function test_responder_missing_items() {
+ let server = await makeServer();
+
+ let fxBmk = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Get Firefox",
+ url: "http://getfirefox.com/",
+ });
+ let tbBmk = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Get Thunderbird",
+ url: "http://getthunderbird.com/",
+ // Pretend we've already synced Thunderbird.
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ });
+
+ await Service.sync();
+ deepEqual(getServerBookmarks(server).keys().sort(), [
+ "menu",
+ "mobile",
+ "toolbar",
+ "unfiled",
+ fxBmk.guid,
+ ].sort(), "Should upload roots and Firefox on first sync");
+
+ _("Request Firefox, Thunderbird, and nonexistent GUID");
+ let request = {
+ request: "upload",
+ ids: [fxBmk.guid, tbBmk.guid, Utils.makeGUID()],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "uploading",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "2"},
+ },
+ ]);
+
+ _("Sync after requesting IDs");
+ await Service.sync();
+ deepEqual(getServerBookmarks(server).keys().sort(), [
+ "menu",
+ "mobile",
+ "toolbar",
+ "unfiled",
+ fxBmk.guid,
+ tbBmk.guid,
+ ].sort(), "Second sync should upload Thunderbird; skip nonexistent");
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "finished",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "2"},
+ },
+ ]);
+
+ await cleanup(server);
+});
+
+add_task(async function test_non_syncable() {
+ let server = await makeServer();
+
+ await Service.sync(); // to create the collections on the server.
+
+ // Creates the left pane queries as a side effect.
+ let leftPaneId = PlacesUIUtils.leftPaneFolderId;
+ _(`Left pane root ID: ${leftPaneId}`);
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ // A child folder of the left pane root, containing queries for the menu,
+ // toolbar, and unfiled queries.
+ let allBookmarksId = PlacesUIUtils.leftPaneQueries.AllBookmarks;
+ let allBookmarksGuid = await PlacesUtils.promiseItemGuid(allBookmarksId);
+
+ let unfiledQueryId = PlacesUIUtils.leftPaneQueries.UnfiledBookmarks;
+ let unfiledQueryGuid = await PlacesUtils.promiseItemGuid(unfiledQueryId);
+
+ // Put the "Bookmarks Menu" on the server to simulate old bugs.
+ let bookmarksMenuQueryId = PlacesUIUtils.leftPaneQueries.BookmarksMenu;
+ let bookmarksMenuQueryGuid = await PlacesUtils.promiseItemGuid(bookmarksMenuQueryId);
+ let collection = getServerBookmarks(server);
+ collection.insert(bookmarksMenuQueryGuid, "doesn't matter");
+
+ // Explicitly request the unfiled and allBookmarksGuid queries; these will
+ // get tombstones. Because the BookmarksMenu is already on the server it
+ // should be removed even though it wasn't requested. We should ignore the
+ // toolbar query as it wasn't explicitly requested and isn't on the server.
+ let request = {
+ request: "upload",
+ ids: [allBookmarksGuid, unfiledQueryGuid],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "uploading",
+ value: undefined,
+ // Tombstones for the 2 items we requested and for bookmarksMenu
+ extra: {flowID: request.flowID, numIDs: "3"},
+ },
+ ]);
+
+ _("Sync to upload tombstones for items");
+ await Service.sync();
+
+ let toolbarQueryId = PlacesUIUtils.leftPaneQueries.BookmarksToolbar;
+ let menuQueryId = PlacesUIUtils.leftPaneQueries.BookmarksMenu;
+ let queryGuids = [
+ allBookmarksGuid,
+ await PlacesUtils.promiseItemGuid(toolbarQueryId),
+ await PlacesUtils.promiseItemGuid(menuQueryId),
+ unfiledQueryGuid,
+ ];
+
+ deepEqual(collection.keys().sort(), [
+ // We always upload roots on the first sync.
+ "menu",
+ "mobile",
+ "toolbar",
+ "unfiled",
+ ...request.ids,
+ bookmarksMenuQueryGuid,
+ ].sort(), "Should upload roots and queries on first sync");
+
+ for (let guid of queryGuids) {
+ let wbo = collection.wbo(guid);
+ if (request.ids.indexOf(guid) >= 0 || guid == bookmarksMenuQueryGuid) {
+ // explicitly requested or already on the server, so should have a tombstone.
+ let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
+ ok(payload.deleted, `Should upload tombstone for left pane query ${guid}`);
+ } else {
+ // not explicitly requested and not on the server at the start, so should
+ // not be on the server now.
+ ok(!wbo, `Should not upload anything for left pane query ${guid}`);
+ }
+ }
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "finished",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "3"},
+ },
+ ]);
+
+ await cleanup(server);
+});
+
+add_task(async function test_folder_descendants() {
+ let server = await makeServer();
+
+ let parentFolder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "Parent folder",
+ });
+ let childFolder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: parentFolder.guid,
+ title: "Child folder",
+ });
+ // This item is in parentFolder and *should not* be uploaded as part of
+ // the repair even though we explicitly request its parent.
+ let existingChildBmk = await PlacesUtils.bookmarks.insert({
+ parentGuid: parentFolder.guid,
+ title: "Get Firefox",
+ url: "http://firefox.com",
+ });
+ // This item is in parentFolder and *should* be uploaded as part of
+ // the repair because we explicitly request its ID.
+ let childSiblingBmk = await PlacesUtils.bookmarks.insert({
+ parentGuid: parentFolder.guid,
+ title: "Get Thunderbird",
+ url: "http://getthunderbird.com",
+ });
+
+ _("Initial sync to upload roots and parent folder");
+ await Service.sync();
+
+ let initialRecordIds = [
+ "menu",
+ "mobile",
+ "toolbar",
+ "unfiled",
+ parentFolder.guid,
+ existingChildBmk.guid,
+ childFolder.guid,
+ childSiblingBmk.guid,
+ ].sort();
+ deepEqual(getServerBookmarks(server).keys().sort(), initialRecordIds,
+ "Should upload roots and partial folder contents on first sync");
+
+ _("Insert missing bookmarks locally to request later");
+ // Note that the fact we insert the bookmarks via PlacesSyncUtils.bookmarks.insert
+ // means that we are pretending Sync itself wrote them, hence they aren't
+ // considered "changed" locally so never get uploaded.
+ let childBmk = await PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ recordId: Utils.makeGUID(),
+ parentRecordId: parentFolder.guid,
+ title: "Get Firefox",
+ url: "http://getfirefox.com",
+ });
+ let grandChildBmk = await PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ recordId: Utils.makeGUID(),
+ parentRecordId: childFolder.guid,
+ title: "Bugzilla",
+ url: "https://bugzilla.mozilla.org",
+ });
+ let grandChildSiblingBmk = await PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ recordId: Utils.makeGUID(),
+ parentRecordId: childFolder.guid,
+ title: "Mozilla",
+ url: "https://mozilla.org",
+ });
+
+ _("Sync again; server contents shouldn't change");
+ await Service.sync();
+ deepEqual(getServerBookmarks(server).keys().sort(), initialRecordIds,
+ "Second sync should not upload missing bookmarks");
+
+ // This assumes the parent record on the server is correct, and the server
+ // is just missing the children. This isn't a correct assumption if the
+ // parent's `children` array is wrong, or if the parent and children disagree.
+ _("Request missing bookmarks");
+ let request = {
+ request: "upload",
+ ids: [
+ // Already on server (but still uploaded as they are explicitly requested)
+ parentFolder.guid,
+ childSiblingBmk.guid,
+ // Explicitly upload these. We should also upload `grandChildBmk`,
+ // since it's a descendant of `parentFolder` and we requested its
+ // ancestor.
+ childBmk.recordId,
+ grandChildSiblingBmk.recordId],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "uploading",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "5"},
+ },
+ ]);
+
+ _("Sync after requesting repair; should upload missing records");
+ await Service.sync();
+ deepEqual(getServerBookmarks(server).keys().sort(), [
+ ...initialRecordIds,
+ childBmk.recordId,
+ grandChildBmk.recordId,
+ grandChildSiblingBmk.recordId,
+ ].sort(), "Third sync should upload requested items");
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "finished",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "5"},
+ },
+ ]);
+
+ await cleanup(server);
+});
+
+// Error handling.
+add_task(async function test_aborts_unknown_request() {
+ let server = await makeServer();
+
+ let request = {
+ request: "not-upload",
+ ids: [],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "aborted",
+ value: undefined,
+ extra: { flowID: request.flowID,
+ reason: "Don't understand request type 'not-upload'",
+ },
+ },
+ ]);
+ await cleanup(server);
+});
+
+add_task(async function test_upload_fail() {
+ let server = await makeServer();
+
+ // Pretend we've already synced this bookmark, so that we can ensure it's
+ // uploaded in response to our repair request.
+ let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Get Firefox",
+ url: "http://getfirefox.com/",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC });
+
+ await Service.sync();
+ let request = {
+ request: "upload",
+ ids: [bm.guid],
+ flowID: Utils.makeGUID(),
+ };
+ let responder = new BookmarkRepairResponder();
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "uploading",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "1"},
+ },
+ ]);
+
+ // This sync would normally upload the item - arrange for it to fail.
+ let engine = Service.engineManager.get("bookmarks");
+ let oldCreateRecord = engine._createRecord;
+ engine._createRecord = async function(id) {
+ return "anything"; // doesn't have an "encrypt"
+ };
+
+ let numFailures = 0;
+ let numSuccesses = 0;
+ function onUploaded(subject, data) {
+ if (data != "bookmarks") {
+ return;
+ }
+ if (subject.failed) {
+ numFailures += 1;
+ } else {
+ numSuccesses += 1;
+ }
+ }
+ Svc.Obs.add("weave:engine:sync:uploaded", onUploaded, this);
+
+ await Service.sync();
+
+ equal(numFailures, 1);
+ equal(numSuccesses, 0);
+
+ // should be no recorded events
+ checkRecordedEvents([]);
+
+ // restore the error injection so next sync succeeds - the repair should
+ // restart
+ engine._createRecord = oldCreateRecord;
+ await responder.repair(request, null);
+
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "uploading",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "1"},
+ },
+ ]);
+
+ await Service.sync();
+ checkRecordedEvents([
+ { object: "repairResponse",
+ method: "finished",
+ value: undefined,
+ extra: {flowID: request.flowID, numIDs: "1"},
+ },
+ ]);
+
+ equal(numFailures, 1);
+ equal(numSuccesses, 1);
+
+ Svc.Obs.remove("weave:engine:sync:uploaded", onUploaded, this);
+ await cleanup(server);
+});
+
+add_task(async function teardown() {
+ Svc.Prefs.reset("engine.bookmarks.validation.enabled");
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/test_bookmark_validator.js
@@ -0,0 +1,404 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://sync-repair/bookmark_validator.jsm");
+Components.utils.import("resource://services-sync/util.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].indexOf("A") >= 0);
+ ok(c.cycles[0].indexOf("B") >= 0);
+});
+
+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.indexOf("B") >= 0);
+ ok(c.multipleParents[0].parents.indexOf("C") >= 0);
+});
+
+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: "B", 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,
+ "annos": [{
+ "name": "Places/SmartBookmark",
+ "flags": 0,
+ "expires": 4,
+ "value": "RecentTags"
+ }],
+ "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,
+ "annos": [{
+ "name": "places/excludeFromBackup",
+ "flags": 0,
+ "expires": 4,
+ "value": 1
+ }, {
+ "name": "PlacesOrganizer/OrganizerFolder",
+ "flags": 0,
+ "expires": 4,
+ "value": 7
+ }],
+ "type": "text/x-moz-place-container",
+ "children": [{
+ "guid": "eeeeeeeeeeee",
+ "title": "History",
+ "annos": [{
+ "name": "places/excludeFromBackup",
+ "flags": 0,
+ "expires": 4,
+ "value": 1
+ }, {
+ "name": "PlacesOrganizer/OrganizerQuery",
+ "flags": 0,
+ "expires": 4,
+ "value": "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"],
+ ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/test_doctor.js
@@ -0,0 +1,188 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { Doctor, REPAIR_ADVANCE_PERIOD } = Cu.import("resource://sync-repair/doctor.jsm", {});
+Cu.import("resource://gre/modules/Services.jsm");
+
+initTestLogging("Trace");
+
+function mockDoctor(mocks) {
+ // Clone the object and put mocks in that.
+ return Object.assign({}, Doctor, mocks);
+}
+
+add_task(async function test_validation_interval() {
+ let now = 1000;
+ let doctor = mockDoctor({
+ _now() {
+ // note that the function being mocked actually returns seconds.
+ return now;
+ },
+ getValidator() {
+ return {
+ validate(e) {
+ return {};
+ }
+ };
+ }
+ });
+
+ let engine = {
+ name: "test-engine",
+ };
+
+ // setup prefs which enable test-engine validation.
+ Services.prefs.setBoolPref("services.sync.engine.test-engine.validation.enabled", true);
+ Services.prefs.setIntPref("services.sync.engine.test-engine.validation.percentageChance", 100);
+ Services.prefs.setIntPref("services.sync.engine.test-engine.validation.maxRecords", 1);
+ // And say we should validate every 10 seconds.
+ Services.prefs.setIntPref("services.sync.engine.test-engine.validation.interval", 10);
+
+ deepEqual(doctor._getEnginesToValidate([engine]), {
+ "test-engine": {
+ engine,
+ maxRecords: 1,
+ }
+ });
+ // We haven't advanced the timestamp, so we should not validate again.
+ deepEqual(doctor._getEnginesToValidate([engine]), {});
+ // Advance our clock by 11 seconds.
+ now += 11;
+ // We should validate again.
+ deepEqual(doctor._getEnginesToValidate([engine]), {
+ "test-engine": {
+ engine,
+ maxRecords: 1,
+ }
+ });
+});
+
+add_task(async function test_repairs_start() {
+ let repairStarted = false;
+ let problems = {
+ missingChildren: ["a", "b", "c"],
+ };
+ let validator = {
+ validate(engine) {
+ return problems;
+ },
+ canValidate() {
+ return Promise.resolve(true);
+ }
+ };
+ let engine = {
+ name: "test-engine"
+ };
+ let requestor = {
+ async startRepairs(validationInfo, flowID) {
+ ok(flowID, "got a flow ID");
+ equal(validationInfo, problems);
+ repairStarted = true;
+ return true;
+ },
+ tryServerOnlyRepairs() {
+ return false;
+ }
+ };
+ let doctor = mockDoctor({
+ _getEnginesToValidate(recentlySyncedEngines) {
+ deepEqual(recentlySyncedEngines, [engine]);
+ return {
+ "test-engine": { engine, maxRecords: -1 }
+ };
+ },
+ _getRepairRequestor(engineName) {
+ equal(engineName, engine.name);
+ return requestor;
+ },
+ _shouldRepair(e) {
+ return true;
+ },
+ _getValidator() {
+ return validator;
+ }
+ });
+ let promiseValidationDone = promiseOneObserver("weave:engine:validate:finish");
+ await doctor.consult([engine]);
+ await promiseValidationDone;
+ ok(repairStarted);
+});
+
+add_task(async function test_repairs_advanced_daily() {
+ let repairCalls = 0;
+ let requestor = {
+ async continueRepairs() {
+ repairCalls++;
+ },
+ tryServerOnlyRepairs() {
+ return false;
+ }
+ };
+ // start now at just after REPAIR_ADVANCE_PERIOD so we do a a first one.
+ let now = REPAIR_ADVANCE_PERIOD + 1;
+ let doctor = mockDoctor({
+ _getEnginesToValidate() {
+ return {}; // no validations.
+ },
+ _runValidators() {
+ // do nothing.
+ },
+ _getAllRepairRequestors() {
+ return {
+ foo: requestor,
+ };
+ },
+ _now() {
+ return now;
+ },
+ });
+ await doctor.consult();
+ equal(repairCalls, 1);
+ now += 10; // 10 seconds later...
+ await doctor.consult();
+ // should not have done another repair yet - it's too soon.
+ equal(repairCalls, 1);
+ // advance our pretend clock by the advance period (eg, day)
+ now += REPAIR_ADVANCE_PERIOD;
+ await doctor.consult();
+ // should have done another repair
+ equal(repairCalls, 2);
+});
+
+add_task(async function test_repairs_skip_if_cant_vaidate() {
+ let validator = {
+ canValidate() {
+ return Promise.resolve(false);
+ },
+ validate() {
+ ok(false, "Shouldn't validate");
+ }
+ };
+ let engine = {
+ name: "test-engine"
+ };
+ let requestor = {
+ async startRepairs(validationInfo, flowID) {
+ ok(false, "Never should start repairs");
+ },
+ tryServerOnlyRepairs() {
+ return false;
+ }
+ };
+ let doctor = mockDoctor({
+ _getEnginesToValidate(recentlySyncedEngines) {
+ deepEqual(recentlySyncedEngines, [engine]);
+ return {
+ "test-engine": { engine, maxRecords: -1 }
+ };
+ },
+ _getRepairRequestor(engineName) {
+ equal(engineName, engine.name);
+ return requestor;
+ },
+ _getValidator() {
+ return validator;
+ }
+ });
+ await doctor.consult([engine]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/test_form_validator.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/engines/forms.js");
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://sync-repair/form_validator.jsm");
+
+function getDummyServerAndClient() {
+ return {
+ server: [
+ {
+ id: "11111",
+ guid: "11111",
+ name: "foo",
+ fieldname: "foo",
+ value: "bar",
+ },
+ {
+ id: "22222",
+ guid: "22222",
+ name: "foo2",
+ fieldname: "foo2",
+ value: "bar2",
+ },
+ {
+ id: "33333",
+ guid: "33333",
+ name: "foo3",
+ fieldname: "foo3",
+ value: "bar3",
+ },
+ ],
+ client: [
+ {
+ id: "11111",
+ guid: "11111",
+ name: "foo",
+ fieldname: "foo",
+ value: "bar",
+ },
+ {
+ id: "22222",
+ guid: "22222",
+ name: "foo2",
+ fieldname: "foo2",
+ value: "bar2",
+ },
+ {
+ id: "33333",
+ guid: "33333",
+ name: "foo3",
+ fieldname: "foo3",
+ value: "bar3",
+ }
+ ]
+ };
+}
+
+function newFormValidator() {
+ let validator = new FormValidator();
+ // Engines aren't registered with engineManager yet.
+ validator.engine = new FormEngine(Weave.Service);
+ return validator;
+}
+
+add_task(async function test_valid() {
+ let { server, client } = getDummyServerAndClient();
+ let validator = newFormValidator();
+ let { problemData, clientRecords, records, deletedRecords } =
+ await validator.compareClientWithServer(client, server);
+ equal(clientRecords.length, 3);
+ equal(records.length, 3);
+ equal(deletedRecords.length, 0);
+ deepEqual(problemData, validator.emptyProblemData());
+});
+
+
+add_task(async function test_formValidatorIgnoresMissingClients() {
+ // Since history form records are not deleted from the server, the
+ // |FormValidator| shouldn't set the |missingClient| flag in |problemData|.
+ let { server, client } = getDummyServerAndClient();
+ client.pop();
+
+ let validator = newFormValidator();
+ let { problemData, clientRecords, records, deletedRecords } =
+ await validator.compareClientWithServer(client, server);
+
+ equal(clientRecords.length, 2);
+ equal(records.length, 3);
+ equal(deletedRecords.length, 0);
+
+ let expected = validator.emptyProblemData();
+ deepEqual(problemData, expected);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/test_password_validator.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/engines/passwords.js");
+Cu.import("resource://sync-repair/password_validator.jsm");
+
+function getDummyServerAndClient() {
+ return {
+ server: [
+ {
+ id: "11111",
+ guid: "11111",
+ hostname: "https://www.11111.com",
+ formSubmitURL: "https://www.11111.com/login",
+ password: "qwerty123",
+ passwordField: "pass",
+ username: "foobar",
+ usernameField: "user",
+ httpRealm: null,
+ },
+ {
+ id: "22222",
+ guid: "22222",
+ hostname: "https://www.22222.org",
+ formSubmitURL: "https://www.22222.org/login",
+ password: "hunter2",
+ passwordField: "passwd",
+ username: "baz12345",
+ usernameField: "user",
+ httpRealm: null,
+ },
+ {
+ id: "33333",
+ guid: "33333",
+ hostname: "https://www.33333.com",
+ formSubmitURL: "https://www.33333.com/login",
+ password: "p4ssw0rd",
+ passwordField: "passwad",
+ username: "quux",
+ usernameField: "user",
+ httpRealm: null,
+ },
+ ],
+ client: [
+ {
+ id: "11111",
+ guid: "11111",
+ hostname: "https://www.11111.com",
+ formSubmitURL: "https://www.11111.com/login",
+ password: "qwerty123",
+ passwordField: "pass",
+ username: "foobar",
+ usernameField: "user",
+ httpRealm: null,
+ },
+ {
+ id: "22222",
+ guid: "22222",
+ hostname: "https://www.22222.org",
+ formSubmitURL: "https://www.22222.org/login",
+ password: "hunter2",
+ passwordField: "passwd",
+ username: "baz12345",
+ usernameField: "user",
+ httpRealm: null,
+
+ },
+ {
+ id: "33333",
+ guid: "33333",
+ hostname: "https://www.33333.com",
+ formSubmitURL: "https://www.33333.com/login",
+ password: "p4ssw0rd",
+ passwordField: "passwad",
+ username: "quux",
+ usernameField: "user",
+ httpRealm: null,
+ }
+ ]
+ };
+}
+
+add_task(async function test_valid() {
+ let { server, client } = getDummyServerAndClient();
+ let validator = new PasswordValidator();
+ let { problemData, clientRecords, records, deletedRecords } =
+ await validator.compareClientWithServer(client, server);
+ equal(clientRecords.length, 3);
+ equal(records.length, 3);
+ equal(deletedRecords.length, 0);
+ deepEqual(problemData, validator.emptyProblemData());
+});
+
+add_task(async function test_missing() {
+ let validator = new PasswordValidator();
+ {
+ let { server, client } = getDummyServerAndClient();
+
+ client.pop();
+
+ let { problemData, clientRecords, records, deletedRecords } =
+ await validator.compareClientWithServer(client, server);
+
+ equal(clientRecords.length, 2);
+ equal(records.length, 3);
+ equal(deletedRecords.length, 0);
+
+ let expected = validator.emptyProblemData();
+ expected.clientMissing.push("33333");
+ deepEqual(problemData, expected);
+ }
+ {
+ let { server, client } = getDummyServerAndClient();
+
+ server.pop();
+
+ let { problemData, clientRecords, records, deletedRecords } =
+ await validator.compareClientWithServer(client, server);
+
+ equal(clientRecords.length, 3);
+ equal(records.length, 2);
+ equal(deletedRecords.length, 0);
+
+ let expected = validator.emptyProblemData();
+ expected.serverMissing.push("33333");
+ deepEqual(problemData, expected);
+ }
+});
+
+
+add_task(async function test_deleted() {
+ let { server, client } = getDummyServerAndClient();
+ let deletionRecord = { id: "444444", guid: "444444", deleted: true };
+
+ server.push(deletionRecord);
+ let validator = new PasswordValidator();
+
+ let { problemData, clientRecords, records, deletedRecords } =
+ await validator.compareClientWithServer(client, server);
+
+ equal(clientRecords.length, 3);
+ equal(records.length, 4);
+ deepEqual(deletedRecords, [deletionRecord]);
+
+ let expected = validator.emptyProblemData();
+ deepEqual(problemData, expected);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/sync-repair/test/unit/xpcshell.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+firefox-appdir = browser
+head = head.js ../../../../../services/sync/tests/unit/head_appinfo.js ../../../../../services/common/tests/unit/head_helpers.js ../../../../../services/sync/tests/unit/head_helpers.js ../../../../../services/sync/tests/unit/head_http_server.js
+[test_bookmark_validator.js]
+[test_form_validator.js]
+[test_password_validator.js]
+[test_doctor.js]
+[test_bookmark_repair.js]
+skip-if = release_or_beta
+run-sequentially = Frequent timeouts, bug 1395148
+[test_bookmark_repair_requestor.js]
+# Repair is enabled only on Aurora and Nightly
+skip-if = release_or_beta
+[test_bookmark_repair_responder.js]
+skip-if = release_or_beta
+run-sequentially = Frequent timeouts, bug 1395148
deleted file mode 100644
--- a/services/sync/modules/bookmark_repair.js
+++ /dev/null
@@ -1,754 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const Cu = Components.utils;
-
-this.EXPORTED_SYMBOLS = ["BookmarkRepairRequestor", "BookmarkRepairResponder"];
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/collection_repair.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/resource.js");
-Cu.import("resource://services-sync/doctor.js");
-Cu.import("resource://services-sync/telemetry.js");
-Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/utils.js");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesSyncUtils",
- "resource://gre/modules/PlacesSyncUtils.jsm");
-
-const log = Log.repository.getLogger("Sync.Engine.Bookmarks.Repair");
-
-const PREF_BRANCH = "services.sync.repairs.bookmarks.";
-
-// How long should we wait after sending a repair request before we give up?
-const RESPONSE_INTERVAL_TIMEOUT = 60 * 60 * 24 * 3; // 3 days
-
-// The maximum number of IDs we will request to be repaired. Beyond this
-// number we assume that trying to repair may do more harm than good and may
-// ask another client to wipe the server and reupload everything. Bug 1341972
-// is tracking that work.
-const MAX_REQUESTED_IDS = 1000;
-
-class AbortRepairError extends Error {
- constructor(reason) {
- super();
- this.reason = reason;
- }
-}
-
-// The states we can be in.
-const STATE = Object.freeze({
- NOT_REPAIRING: "",
-
- // We need to try to find another client to use.
- NEED_NEW_CLIENT: "repair.need-new-client",
-
- // We've sent the first request to a client.
- SENT_REQUEST: "repair.sent",
-
- // We've retried a request to a client.
- SENT_SECOND_REQUEST: "repair.sent-again",
-
- // There were no problems, but we've gone as far as we can.
- FINISHED: "repair.finished",
-
- // We've found an error that forces us to abort this entire repair cycle.
- ABORTED: "repair.aborted",
-});
-
-// The preferences we use to hold our state.
-const PREF = Object.freeze({
- // If a repair is in progress, this is the generated GUID for the "flow ID".
- REPAIR_ID: "flowID",
-
- // The IDs we are currently trying to obtain via the repair process, space sep'd.
- REPAIR_MISSING_IDS: "ids",
-
- // The ID of the client we're currently trying to get the missing items from.
- REPAIR_CURRENT_CLIENT: "currentClient",
-
- // The IDs of the clients we've previously tried to get the missing items
- // from, space sep'd.
- REPAIR_PREVIOUS_CLIENTS: "previousClients",
-
- // The time, in seconds, when we initiated the most recent client request.
- REPAIR_WHEN: "when",
-
- // Our current state.
- CURRENT_STATE: "state",
-});
-
-class BookmarkRepairRequestor extends CollectionRepairRequestor {
- constructor(service = null) {
- super(service);
- this.prefs = new Preferences(PREF_BRANCH);
- }
-
- /* Check if any other clients connected to our account are current performing
- a repair. A thin wrapper which exists mainly for mocking during tests.
- */
- anyClientsRepairing(flowID) {
- return Doctor.anyClientsRepairing(this.service, "bookmarks", flowID);
- }
-
- /* Return a set of IDs we should request.
- */
- getProblemIDs(validationInfo) {
- // Set of ids of "known bad records". Many of the validation issues will
- // report duplicates -- if the server is missing a record, it is unlikely
- // to cause only a single problem.
- let ids = new Set();
-
- // Note that we allow any of the validation problem fields to be missing so
- // that tests have a slightly easier time, hence the `|| []` in each loop.
-
- // Missing children records when the parent exists but a child doesn't.
- for (let { parent, child } of validationInfo.problems.missingChildren || []) {
- // We can't be sure if the child is missing or our copy of the parent is
- // wrong, so request both
- ids.add(parent);
- ids.add(child);
- }
- if (ids.size > MAX_REQUESTED_IDS) {
- return ids; // might as well give up here - we aren't going to repair.
- }
-
- // Orphans are when the child exists but the parent doesn't.
- // This could either be a problem in the child (it's wrong about the node
- // that should be its parent), or the parent could simply be missing.
- for (let { parent, id } of validationInfo.problems.orphans || []) {
- // Request both, to handle both cases
- ids.add(id);
- ids.add(parent);
- }
- if (ids.size > MAX_REQUESTED_IDS) {
- return ids; // might as well give up here - we aren't going to repair.
- }
-
- // Entries where we have the parent but we have a record from the server that
- // claims the child was deleted.
- for (let { parent, child } of validationInfo.problems.deletedChildren || []) {
- // Request both, since we don't know if it's a botched deletion or revival
- ids.add(parent);
- ids.add(child);
- }
- if (ids.size > MAX_REQUESTED_IDS) {
- return ids; // might as well give up here - we aren't going to repair.
- }
-
- // Entries where the child references a parent that we don't have, but we
- // have a record from the server that claims the parent was deleted.
- for (let { parent, child } of validationInfo.problems.deletedParents || []) {
- // Request both, since we don't know if it's a botched deletion or revival
- ids.add(parent);
- ids.add(child);
- }
- if (ids.size > MAX_REQUESTED_IDS) {
- return ids; // might as well give up here - we aren't going to repair.
- }
-
- // Cases where the parent and child disagree about who the parent is.
- for (let { parent, child } of validationInfo.problems.parentChildMismatches || []) {
- // Request both, since we don't know which is right.
- ids.add(parent);
- ids.add(child);
- }
- if (ids.size > MAX_REQUESTED_IDS) {
- return ids; // might as well give up here - we aren't going to repair.
- }
-
- // Cases where multiple parents reference a child. We re-request both the
- // child, and all the parents that think they own it. This may be more than
- // we need, but I don't think we can safely make the assumption that the
- // child is right.
- for (let { parents, child } of validationInfo.problems.multipleParents || []) {
- for (let parent of parents) {
- ids.add(parent);
- }
- ids.add(child);
- }
-
- return ids;
- }
-
- _countServerOnlyFixableProblems(validationInfo) {
- const fixableProblems = ["clientMissing", "serverMissing", "serverDeleted"];
- return fixableProblems.reduce((numProblems, problemLabel) => {
- return numProblems + validationInfo.problems[problemLabel].length;
- }, 0);
- }
-
- tryServerOnlyRepairs(validationInfo) {
- if (this._countServerOnlyFixableProblems(validationInfo) == 0) {
- return false;
- }
- let engine = this.service.engineManager.get("bookmarks");
- for (let id of validationInfo.problems.serverMissing) {
- engine.addForWeakUpload(id);
- }
- let toFetch = engine.toFetch.concat(validationInfo.problems.clientMissing,
- validationInfo.problems.serverDeleted);
- engine.toFetch = Array.from(new Set(toFetch));
- return true;
- }
-
- /* See if the repairer is willing and able to begin a repair process given
- the specified validation information.
- Returns true if a repair was started and false otherwise.
- */
- async startRepairs(validationInfo, flowID) {
- if (this._currentState != STATE.NOT_REPAIRING) {
- log.info(`Can't start a repair - repair with ID ${this._flowID} is already in progress`);
- return false;
- }
-
- let ids = this.getProblemIDs(validationInfo);
- if (ids.size > MAX_REQUESTED_IDS) {
- log.info("Not starting a repair as there are over " + MAX_REQUESTED_IDS + " problems");
- let extra = { flowID, reason: `too many problems: ${ids.size}` };
- this.service.recordTelemetryEvent("repair", "aborted", undefined, extra);
- return false;
- }
-
- if (ids.size == 0) {
- log.info("Not starting a repair as there are no problems");
- return false;
- }
-
- if (this.anyClientsRepairing()) {
- log.info("Can't start repair, since other clients are already repairing bookmarks");
- let extra = { flowID, reason: "other clients repairing" };
- this.service.recordTelemetryEvent("repair", "aborted", undefined, extra);
- return false;
- }
-
- log.info(`Starting a repair, looking for ${ids.size} missing item(s)`);
- // setup our prefs to indicate we are on our way.
- this._flowID = flowID;
- this._currentIDs = Array.from(ids);
- this._currentState = STATE.NEED_NEW_CLIENT;
- this.service.recordTelemetryEvent("repair", "started", undefined, { flowID, numIDs: ids.size.toString() });
- return this.continueRepairs();
- }
-
- /* Work out what state our current repair request is in, and whether it can
- proceed to a new state.
- Returns true if we could continue the repair - even if the state didn't
- actually move. Returns false if we aren't actually repairing.
- */
- async continueRepairs(response = null) {
- // Note that "ABORTED" and "FINISHED" should never be current when this
- // function returns - this function resets to NOT_REPAIRING in those cases.
- if (this._currentState == STATE.NOT_REPAIRING) {
- return false;
- }
-
- let state, newState;
- let abortReason;
- // we loop until the state doesn't change - but enforce a max of 10 times
- // to prevent errors causing infinite loops.
- for (let i = 0; i < 10; i++) {
- state = this._currentState;
- log.info("continueRepairs starting with state", state);
- try {
- newState = await this._continueRepairs(state, response);
- log.info("continueRepairs has next state", newState);
- } catch (ex) {
- if (!(ex instanceof AbortRepairError)) {
- throw ex;
- }
- log.info(`Repair has been aborted: ${ex.reason}`);
- newState = STATE.ABORTED;
- abortReason = ex.reason;
- }
-
- if (newState == STATE.ABORTED) {
- break;
- }
-
- this._currentState = newState;
- Services.prefs.savePrefFile(null); // flush prefs.
- if (state == newState) {
- break;
- }
- }
- if (state != newState) {
- log.error("continueRepairs spun without getting a new state");
- }
- if (newState == STATE.FINISHED || newState == STATE.ABORTED) {
- let object = newState == STATE.FINISHED ? "finished" : "aborted";
- let extra = {
- flowID: this._flowID,
- numIDs: this._currentIDs.length.toString(),
- };
- if (abortReason) {
- extra.reason = abortReason;
- }
- this.service.recordTelemetryEvent("repair", object, undefined, extra);
- // reset our state and flush our prefs.
- this.prefs.resetBranch();
- Services.prefs.savePrefFile(null); // flush prefs.
- }
- return true;
- }
-
- async _continueRepairs(state, response = null) {
- if (this.anyClientsRepairing(this._flowID)) {
- throw new AbortRepairError("other clients repairing");
- }
- switch (state) {
- case STATE.SENT_REQUEST:
- case STATE.SENT_SECOND_REQUEST:
- let flowID = this._flowID;
- let clientID = this._currentRemoteClient;
- if (!clientID) {
- throw new AbortRepairError(`In state ${state} but have no client IDs listed`);
- }
- if (response) {
- // We got an explicit response - let's see how we went.
- state = this._handleResponse(state, response);
- break;
- }
- // So we've sent a request - and don't yet have a response. See if the
- // client we sent it to has removed it from its list (ie, whether it
- // has synced since we wrote the request.)
- let client = this.service.clientsEngine.remoteClient(clientID);
- if (!client) {
- // hrmph - the client has disappeared.
- log.info(`previously requested client "${clientID}" has vanished - moving to next step`);
- state = STATE.NEED_NEW_CLIENT;
- let extra = {
- deviceID: this.service.identity.hashedDeviceID(clientID),
- flowID,
- };
- this.service.recordTelemetryEvent("repair", "abandon", "missing", extra);
- break;
- }
- if ((await this._isCommandPending(clientID, flowID))) {
- // So the command we previously sent is still queued for the client
- // (ie, that client is yet to have synced). Let's see if we should
- // give up on that client.
- let lastRequestSent = this.prefs.get(PREF.REPAIR_WHEN);
- let timeLeft = lastRequestSent + RESPONSE_INTERVAL_TIMEOUT - this._now();
- if (timeLeft <= 0) {
- log.info(`previous request to client ${clientID} is pending, but has taken too long`);
- state = STATE.NEED_NEW_CLIENT;
- // XXX - should we remove the command?
- let extra = {
- deviceID: this.service.identity.hashedDeviceID(clientID),
- flowID,
- };
- this.service.recordTelemetryEvent("repair", "abandon", "silent", extra);
- break;
- }
- // Let's continue to wait for that client to respond.
- log.trace(`previous request to client ${clientID} has ${timeLeft} seconds before we give up on it`);
- break;
- }
- // The command isn't pending - if this was the first request, we give
- // it another go (as that client may have cleared the command but is yet
- // to complete the sync)
- // XXX - note that this is no longer true - the responders don't remove
- // their command until they have written a response. This might mean
- // we could drop the entire STATE.SENT_SECOND_REQUEST concept???
- if (state == STATE.SENT_REQUEST) {
- log.info(`previous request to client ${clientID} was removed - trying a second time`);
- state = STATE.SENT_SECOND_REQUEST;
- await this._writeRequest(clientID);
- } else {
- // this was the second time around, so give up on this client
- log.info(`previous 2 requests to client ${clientID} were removed - need a new client`);
- state = STATE.NEED_NEW_CLIENT;
- }
- break;
-
- case STATE.NEED_NEW_CLIENT:
- // We need to find a new client to request.
- let newClientID = this._findNextClient();
- if (!newClientID) {
- state = STATE.FINISHED;
- break;
- }
- this._addToPreviousRemoteClients(this._currentRemoteClient);
- this._currentRemoteClient = newClientID;
- await this._writeRequest(newClientID);
- state = STATE.SENT_REQUEST;
- break;
-
- case STATE.ABORTED:
- break; // our caller will take the abort action.
-
- case STATE.FINISHED:
- break;
-
- case STATE.NOT_REPAIRING:
- // No repair is in progress. This is a common case, so only log trace.
- log.trace("continue repairs called but no repair in progress.");
- break;
-
- default:
- log.error(`continue repairs finds itself in an unknown state ${state}`);
- state = STATE.ABORTED;
- break;
-
- }
- return state;
- }
-
- /* Handle being in the SENT_REQUEST or SENT_SECOND_REQUEST state with an
- explicit response.
- */
- _handleResponse(state, response) {
- let clientID = this._currentRemoteClient;
- let flowID = this._flowID;
-
- if (response.flowID != flowID || response.clientID != clientID ||
- response.request != "upload") {
- log.info("got a response to a different repair request", response);
- // hopefully just a stale request that finally came in (either from
- // an entirely different repair flow, or from a client we've since
- // given up on.) It doesn't mean we need to abort though...
- return state;
- }
- // Pull apart the response and see if it provided everything we asked for.
- let remainingIDs = Array.from(CommonUtils.difference(this._currentIDs, response.ids));
- log.info(`repair response from ${clientID} provided "${response.ids}", remaining now "${remainingIDs}"`);
- this._currentIDs = remainingIDs;
- if (remainingIDs.length) {
- // try a new client for the remaining ones.
- state = STATE.NEED_NEW_CLIENT;
- } else {
- state = STATE.FINISHED;
- }
- // record telemetry about this
- let extra = {
- deviceID: this.service.identity.hashedDeviceID(clientID),
- flowID,
- numIDs: response.ids.length.toString(),
- };
- this.service.recordTelemetryEvent("repair", "response", "upload", extra);
- return state;
- }
-
- /* Issue a repair request to a specific client.
- */
- async _writeRequest(clientID) {
- log.trace("writing repair request to client", clientID);
- let ids = this._currentIDs;
- if (!ids) {
- throw new AbortRepairError("Attempting to write a request, but there are no IDs");
- }
- let flowID = this._flowID;
- // Post a command to that client.
- let request = {
- collection: "bookmarks",
- request: "upload",
- requestor: this.service.clientsEngine.localID,
- ids,
- flowID,
- };
- await this.service.clientsEngine.sendCommand("repairRequest", [request], clientID, { flowID });
- this.prefs.set(PREF.REPAIR_WHEN, Math.floor(this._now()));
- // record telemetry about this
- let extra = {
- deviceID: this.service.identity.hashedDeviceID(clientID),
- flowID,
- numIDs: ids.length.toString(),
- };
- this.service.recordTelemetryEvent("repair", "request", "upload", extra);
- }
-
- _findNextClient() {
- let alreadyDone = this._getPreviousRemoteClients();
- alreadyDone.push(this._currentRemoteClient);
- let remoteClients = this.service.clientsEngine.remoteClients;
- // we want to consider the most-recently synced clients first.
- remoteClients.sort((a, b) => b.serverLastModified - a.serverLastModified);
- for (let client of remoteClients) {
- log.trace("findNextClient considering", client);
- if (alreadyDone.indexOf(client.id) == -1 && this._isSuitableClient(client)) {
- return client.id;
- }
- }
- log.trace("findNextClient found no client");
- return null;
- }
-
- /* Is the passed client record suitable as a repair responder?
- */
- _isSuitableClient(client) {
- // filter only desktop firefox running > 53 (ie, any 54)
- return (client.type == DEVICE_TYPE_DESKTOP &&
- Services.vc.compare(client.version, 53) > 0);
- }
-
- /* Is our command still in the "commands" queue for the specific client?
- */
- async _isCommandPending(clientID, flowID) {
- // getClientCommands() is poorly named - it's only outgoing commands
- // from us we have yet to write. For our purposes, we want to check
- // them and commands previously written (which is in .commands)
- let clientCommands = await this.service.clientsEngine.getClientCommands(clientID);
- let commands = [...clientCommands,
- ...this.service.clientsEngine.remoteClient(clientID).commands || []];
- for (let command of commands) {
- if (command.command != "repairRequest" || command.args.length != 1) {
- continue;
- }
- let arg = command.args[0];
- if (arg.collection == "bookmarks" && arg.request == "upload" &&
- arg.flowID == flowID) {
- return true;
- }
- }
- return false;
- }
-
- // accessors for our prefs.
- get _currentState() {
- return this.prefs.get(PREF.CURRENT_STATE, STATE.NOT_REPAIRING);
- }
- set _currentState(newState) {
- this.prefs.set(PREF.CURRENT_STATE, newState);
- }
-
- get _currentIDs() {
- let ids = this.prefs.get(PREF.REPAIR_MISSING_IDS, "");
- return ids.length ? ids.split(" ") : [];
- }
- set _currentIDs(arrayOfIDs) {
- this.prefs.set(PREF.REPAIR_MISSING_IDS, arrayOfIDs.join(" "));
- }
-
- get _currentRemoteClient() {
- return this.prefs.get(PREF.REPAIR_CURRENT_CLIENT);
- }
- set _currentRemoteClient(clientID) {
- this.prefs.set(PREF.REPAIR_CURRENT_CLIENT, clientID);
- }
-
- get _flowID() {
- return this.prefs.get(PREF.REPAIR_ID);
- }
- set _flowID(val) {
- this.prefs.set(PREF.REPAIR_ID, val);
- }
-
- // use a function for this pref to offer somewhat sane semantics.
- _getPreviousRemoteClients() {
- let alreadyDone = this.prefs.get(PREF.REPAIR_PREVIOUS_CLIENTS, "");
- return alreadyDone.length ? alreadyDone.split(" ") : [];
- }
- _addToPreviousRemoteClients(clientID) {
- let arrayOfClientIDs = this._getPreviousRemoteClients();
- arrayOfClientIDs.push(clientID);
- this.prefs.set(PREF.REPAIR_PREVIOUS_CLIENTS, arrayOfClientIDs.join(" "));
- }
-
- /* Used for test mocks.
- */
- _now() {
- // We use the server time, which is SECONDS
- return Resource.serverTime;
- }
-}
-
-/* An object that responds to repair requests initiated by some other device.
-*/
-class BookmarkRepairResponder extends CollectionRepairResponder {
- async repair(request, rawCommand) {
- if (request.request != "upload") {
- await this._abortRepair(request, rawCommand,
- `Don't understand request type '${request.request}'`);
- return;
- }
-
- // Note that we don't try and guard against multiple repairs being in
- // progress as we don't do anything too smart that could cause problems,
- // but just upload items. If we get any smarter we should re-think this
- // (but when we do, note that checking this._currentState isn't enough as
- // this responder is not a singleton)
-
- this._currentState = {
- request,
- rawCommand,
- processedCommand: false,
- ids: [],
- };
-
- try {
- let engine = this.service.engineManager.get("bookmarks");
- let { toUpload, toDelete } = await this._fetchItemsToUpload(request);
-
- if (toUpload.size || toDelete.size) {
- log.debug(`repair request will upload ${toUpload.size} items and delete ${toDelete.size} items`);
- // whew - now add these items to the tracker "weakly" (ie, they will not
- // persist in the case of a restart, but that's OK - we'll then end up here
- // again) and also record them in the response we send back.
- for (let id of toUpload) {
- engine.addForWeakUpload(id);
- this._currentState.ids.push(id);
- }
- for (let id of toDelete) {
- engine.addForWeakUpload(id, { forceTombstone: true });
- this._currentState.ids.push(id);
- }
-
- // We have arranged for stuff to be uploaded, so wait until that's done.
- Svc.Obs.add("weave:engine:sync:uploaded", this.onUploaded, this);
- // and record in telemetry that we got this far - just incase we never
- // end up doing the upload for some obscure reason.
- let eventExtra = {
- flowID: request.flowID,
- numIDs: this._currentState.ids.length.toString(),
- };
- this.service.recordTelemetryEvent("repairResponse", "uploading", undefined, eventExtra);
- } else {
- // We were unable to help with the repair, so report that we are done.
- await this._finishRepair();
- }
- } catch (ex) {
- if (Async.isShutdownException(ex)) {
- // this repair request will be tried next time.
- throw ex;
- }
- // On failure, we still write a response so the requestor knows to move
- // on, but we record the failure reason in telemetry.
- log.error("Failed to respond to the repair request", ex);
- this._currentState.failureReason = SyncTelemetry.transformError(ex);
- await this._finishRepair();
- }
- }
-
- async _fetchItemsToUpload(request) {
- let toUpload = new Set(); // items we will upload.
- let toDelete = new Set(); // items we will delete.
-
- let requested = new Set(request.ids);
-
- let engine = this.service.engineManager.get("bookmarks");
- // Determine every item that may be impacted by the requested IDs - eg,
- // this may include children if a requested ID is a folder.
- // Turn an array of { recordId, syncable } into a map of recordId -> syncable.
- let repairable = await PlacesSyncUtils.bookmarks.fetchRecordIdsForRepair(request.ids);
- if (repairable.length == 0) {
- // server will get upset if we request an empty set, and we can't do
- // anything in that case, so bail now.
- return { toUpload, toDelete };
- }
-
- // which of these items exist on the server?
- let itemSource = engine.itemSource();
- itemSource.ids = repairable.map(item => item.recordId);
- log.trace(`checking the server for items`, itemSource.ids);
- let itemsResponse = await itemSource.get();
- // If the response failed, don't bother trying to parse the output.
- // Throwing here means we abort the repair, which isn't ideal for transient
- // errors (eg, no network, 500 service outage etc), but we don't currently
- // have a sane/safe way to try again later (we'd need to implement a kind
- // of timeout, otherwise we might end up retrying forever and never remove
- // our request command.) Bug 1347805.
- if (!itemsResponse.success) {
- throw new Error(`request for server IDs failed: ${itemsResponse.status}`);
- }
- let existRemotely = new Set(JSON.parse(itemsResponse));
- // We need to be careful about handing the requested items:
- // * If the item exists locally but isn't in the tree of items we sync
- // (eg, it might be a left-pane item or similar, we write a tombstone.
- // * If the item exists locally as a folder, we upload the folder and any
- // children which don't exist on the server. (Note that we assume the
- // parents *do* exist)
- // Bug 1343101 covers additional issues we might repair in the future.
- for (let { recordId: id, syncable } of repairable) {
- if (requested.has(id)) {
- if (syncable) {
- log.debug(`repair request to upload item '${id}' which exists locally; uploading`);
- toUpload.add(id);
- } else {
- // explicitly requested and not syncable, so tombstone.
- log.debug(`repair request to upload item '${id}' but it isn't under a syncable root; writing a tombstone`);
- toDelete.add(id);
- }
- // The item wasn't explicitly requested - only upload if it is syncable
- // and doesn't exist on the server.
- } else if (syncable && !existRemotely.has(id)) {
- log.debug(`repair request found related item '${id}' which isn't on the server; uploading`);
- toUpload.add(id);
- } else if (!syncable && existRemotely.has(id)) {
- log.debug(`repair request found non-syncable related item '${id}' on the server; writing a tombstone`);
- toDelete.add(id);
- } else {
- log.debug(`repair request found related item '${id}' which we will not upload; ignoring`);
- }
- }
- return { toUpload, toDelete };
- }
-
- onUploaded(subject, data) {
- if (data != "bookmarks") {
- return;
- }
- Svc.Obs.remove("weave:engine:sync:uploaded", this.onUploaded, this);
- if (subject.failed) {
- return;
- }
- log.debug(`bookmarks engine has uploaded stuff - creating a repair response`, subject);
- Async.promiseSpinningly(this._finishRepair());
- }
-
- async _finishRepair() {
- let clientsEngine = this.service.clientsEngine;
- let flowID = this._currentState.request.flowID;
- let response = {
- request: this._currentState.request.request,
- collection: "bookmarks",
- clientID: clientsEngine.localID,
- flowID,
- ids: this._currentState.ids,
- };
- let clientID = this._currentState.request.requestor;
- await clientsEngine.sendCommand("repairResponse", [response], clientID, { flowID });
- // and nuke the request from our client.
- await clientsEngine.removeLocalCommand(this._currentState.rawCommand);
- let eventExtra = {
- flowID,
- numIDs: response.ids.length.toString(),
- };
- if (this._currentState.failureReason) {
- // *sob* - recording this in "extra" means the value must be a string of
- // max 85 chars.
- eventExtra.failureReason = JSON.stringify(this._currentState.failureReason).substring(0, 85);
- this.service.recordTelemetryEvent("repairResponse", "failed", undefined, eventExtra);
- } else {
- this.service.recordTelemetryEvent("repairResponse", "finished", undefined, eventExtra);
- }
- this._currentState = null;
- }
-
- async _abortRepair(request, rawCommand, why) {
- log.warn(`aborting repair request: ${why}`);
- await this.service.clientsEngine.removeLocalCommand(rawCommand);
- // record telemetry for this.
- let eventExtra = {
- flowID: request.flowID,
- reason: why,
- };
- this.service.recordTelemetryEvent("repairResponse", "aborted", undefined, eventExtra);
- // We could also consider writing a response here so the requestor can take
- // some immediate action rather than timing out, but we abort only in cases
- // that should be rare, so let's wait and see what telemetry tells us.
- }
-}
-
-/* Exposed in case another module needs to understand our state.
-*/
-BookmarkRepairRequestor.STATE = STATE;
-BookmarkRepairRequestor.PREF = PREF;
deleted file mode 100644
--- a/services/sync/modules/collection_repair.js
+++ /dev/null
@@ -1,139 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://services-sync/main.js");
-
-XPCOMUtils.defineLazyModuleGetter(this, "BookmarkRepairRequestor",
- "resource://services-sync/bookmark_repair.js");
-
-this.EXPORTED_SYMBOLS = ["getRepairRequestor", "getAllRepairRequestors",
- "CollectionRepairRequestor",
- "getRepairResponder",
- "CollectionRepairResponder"];
-
-// The individual requestors/responders, lazily loaded.
-const REQUESTORS = {
- bookmarks: ["bookmark_repair.js", "BookmarkRepairRequestor"],
-};
-
-const RESPONDERS = {
- bookmarks: ["bookmark_repair.js", "BookmarkRepairResponder"],
-};
-
-// Should we maybe enforce the requestors being a singleton?
-function _getRepairConstructor(which, collection) {
- if (!(collection in which)) {
- return null;
- }
- let [modname, symbolname] = which[collection];
- let ns = {};
- Cu.import("resource://services-sync/" + modname, ns);
- return ns[symbolname];
-}
-
-function getRepairRequestor(collection) {
- let ctor = _getRepairConstructor(REQUESTORS, collection);
- if (!ctor) {
- return null;
- }
- return new ctor();
-}
-
-function getAllRepairRequestors() {
- let result = {};
- for (let collection of Object.keys(REQUESTORS)) {
- let ctor = _getRepairConstructor(REQUESTORS, collection);
- result[collection] = new ctor();
- }
- return result;
-}
-
-function getRepairResponder(collection) {
- let ctor = _getRepairConstructor(RESPONDERS, collection);
- if (!ctor) {
- return null;
- }
- return new ctor();
-}
-
-// The abstract classes.
-class CollectionRepairRequestor {
- constructor(service = null) {
- // allow service to be mocked in tests.
- this.service = service || Weave.Service;
- }
-
- /* Try to resolve some issues with the server without involving other clients.
- Returns true if we repaired some items.
-
- @param validationInfo {Object}
- The validation info as returned by the collection's validator.
-
- */
- tryServerOnlyRepairs(validationInfo) {
- return false;
- }
-
- /* See if the repairer is willing and able to begin a repair process given
- the specified validation information.
- Returns true if a repair was started and false otherwise.
-
- @param validationInfo {Object}
- The validation info as returned by the collection's validator.
-
- @param flowID {String}
- A guid that uniquely identifies this repair process for this
- collection, and which should be sent to any requestors and
- reported in telemetry.
-
- */
- async startRepairs(validationInfo, flowID) {
- throw new Error("not implemented");
- }
-
- /* Work out what state our current repair request is in, and whether it can
- proceed to a new state.
- Returns true if we could continue the repair - even if the state didn't
- actually move. Returns false if we aren't actually repairing.
-
- @param responseInfo {Object}
- An optional response to a previous repair request, as returned
- by a remote repair responder.
-
- */
- async continueRepairs(responseInfo = null) {
- throw new Error("not implemented");
- }
-}
-
-class CollectionRepairResponder {
- constructor(service = null) {
- // allow service to be mocked in tests.
- this.service = service || Weave.Service;
- }
-
- /* Take some action in response to a repair request. Returns a promise that
- resolves once the repair process has started, or rejects if there
- was an error starting the repair.
-
- Note that when the promise resolves the repair is not yet complete - at
- some point in the future the repair will auto-complete, at which time
- |rawCommand| will be removed from the list of client commands for this
- client.
-
- @param request {Object}
- The repair request as sent by another client.
-
- @param rawCommand {Object}
- The command object as stored in the clients engine, and which
- will be automatically removed once a repair completes.
- */
- async repair(request, rawCommand) {
- throw new Error("not implemented");
- }
-}
deleted file mode 100644
--- a/services/sync/modules/collection_validator.js
+++ /dev/null
@@ -1,231 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const Cu = Components.utils;
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Async",
- "resource://services-common/async.js");
-
-
-this.EXPORTED_SYMBOLS = ["CollectionValidator", "CollectionProblemData"];
-
-class CollectionProblemData {
- constructor() {
- this.missingIDs = 0;
- this.duplicates = [];
- this.clientMissing = [];
- this.serverMissing = [];
- this.serverDeleted = [];
- this.serverUnexpected = [];
- this.differences = [];
- }
-
- /**
- * Produce a list summarizing problems found. Each entry contains {name, count},
- * where name is the field name for the problem, and count is the number of times
- * the problem was encountered.
- *
- * Validation has failed if all counts are not 0.
- */
- getSummary() {
- return [
- { name: "clientMissing", count: this.clientMissing.length },
- { name: "serverMissing", count: this.serverMissing.length },
- { name: "serverDeleted", count: this.serverDeleted.length },
- { name: "serverUnexpected", count: this.serverUnexpected.length },
- { name: "differences", count: this.differences.length },
- { name: "missingIDs", count: this.missingIDs },
- { name: "duplicates", count: this.duplicates.length }
- ];
- }
-}
-
-class CollectionValidator {
- // Construct a generic collection validator. This is intended to be called by
- // subclasses.
- // - name: Name of the engine
- // - idProp: Property that identifies a record. That is, if a client and server
- // record have the same value for the idProp property, they should be
- // compared against eachother.
- // - props: Array of properties that should be compared
- constructor(name, idProp, props) {
- this.name = name;
- this.props = props;
- this.idProp = idProp;
-
- // This property deals with the fact that form history records are never
- // deleted from the server. The FormValidator subclass needs to ignore the
- // client missing records, and it uses this property to achieve it -
- // (Bug 1354016).
- this.ignoresMissingClients = false;
- }
-
- // Should a custom ProblemData type be needed, return it here.
- emptyProblemData() {
- return new CollectionProblemData();
- }
-
- async getServerItems(engine) {
- let collection = engine.itemSource();
- let collectionKey = engine.service.collectionKeys.keyForCollection(engine.name);
- collection.full = true;
- let result = await collection.getBatched();
- if (!result.response.success) {
- throw result.response;
- }
- let maybeYield = Async.jankYielder();
- let cleartexts = [];
- for (let record of result.records) {
- await maybeYield();
- await record.decrypt(collectionKey);
- cleartexts.push(record.cleartext);
- }
- return cleartexts;
- }
-
- // Should return a promise that resolves to an array of client items.
- getClientItems() {
- return Promise.reject("Must implement");
- }
-
- /**
- * Can we guarantee validation will fail with a reason that isn't actually a
- * problem? For example, if we know there are pending changes left over from
- * the last sync, this should resolve to false. By default resolves to true.
- */
- async canValidate() {
- return true;
- }
-
- // Turn the client item into something that can be compared with the server item,
- // and is also safe to mutate.
- normalizeClientItem(item) {
- return Cu.cloneInto(item, {});
- }
-
- // Turn the server item into something that can be easily compared with the client
- // items.
- async normalizeServerItem(item) {
- return item;
- }
-
- // Return whether or not a server item should be present on the client. Expected
- // to be overridden.
- clientUnderstands(item) {
- return true;
- }
-
- // Return whether or not a client item should be present on the server. Expected
- // to be overridden
- syncedByClient(item) {
- return true;
- }
-
- // Compare the server item and the client item, and return a list of property
- // names that are different. Can be overridden if needed.
- getDifferences(client, server) {
- let differences = [];
- for (let prop of this.props) {
- let clientProp = client[prop];
- let serverProp = server[prop];
- if ((clientProp || "") !== (serverProp || "")) {
- differences.push(prop);
- }
- }
- return differences;
- }
-
- // Returns an object containing
- // problemData: an instance of the class returned by emptyProblemData(),
- // clientRecords: Normalized client records
- // records: Normalized server records,
- // deletedRecords: Array of ids that were marked as deleted by the server.
- async compareClientWithServer(clientItems, serverItems) {
- let maybeYield = Async.jankYielder();
- const clientRecords = [];
- for (let item of clientItems) {
- await maybeYield();
- clientRecords.push(this.normalizeClientItem(item));
- }
- const serverRecords = [];
- for (let item of serverItems) {
- await maybeYield();
- serverRecords.push((await this.normalizeServerItem(item)));
- }
- let problems = this.emptyProblemData();
- let seenServer = new Map();
- let serverDeleted = new Set();
- let allRecords = new Map();
-
- for (let record of serverRecords) {
- let id = record[this.idProp];
- if (!id) {
- ++problems.missingIDs;
- continue;
- }
- if (record.deleted) {
- serverDeleted.add(record);
- } else {
- let possibleDupe = seenServer.get(id);
- if (possibleDupe) {
- problems.duplicates.push(id);
- } else {
- seenServer.set(id, record);
- allRecords.set(id, { server: record, client: null, });
- }
- record.understood = this.clientUnderstands(record);
- }
- }
-
- let seenClient = new Map();
- for (let record of clientRecords) {
- let id = record[this.idProp];
- record.shouldSync = this.syncedByClient(record);
- seenClient.set(id, record);
- let combined = allRecords.get(id);
- if (combined) {
- combined.client = record;
- } else {
- allRecords.set(id, { client: record, server: null });
- }
- }
-
- for (let [id, { server, client }] of allRecords) {
- if (!client && !server) {
- throw new Error("Impossible: no client or server record for " + id);
- } else if (server && !client) {
- if (!this.ignoresMissingClients && server.understood) {
- problems.clientMissing.push(id);
- }
- } else if (client && !server) {
- if (client.shouldSync) {
- problems.serverMissing.push(id);
- }
- } else {
- if (!client.shouldSync) {
- if (!problems.serverUnexpected.includes(id)) {
- problems.serverUnexpected.push(id);
- }
- continue;
- }
- let differences = this.getDifferences(client, server);
- if (differences && differences.length) {
- problems.differences.push({ id, differences });
- }
- }
- }
- return {
- problemData: problems,
- clientRecords,
- records: serverRecords,
- deletedRecords: [...serverDeleted]
- };
- }
-}
-
-// Default to 0, some engines may override.
-CollectionValidator.prototype.version = 0;
deleted file mode 100644
--- a/services/sync/modules/doctor.js
+++ /dev/null
@@ -1,260 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// A doctor for our collections. She can be asked to make a consultation, and
-// may just diagnose an issue without attempting to cure it, may diagnose and
-// attempt to cure, or may decide she is overworked and underpaid.
-// Or something - naming is hard :)
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["Doctor"];
-
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-common/observers.js");
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/resource.js");
-
-Cu.import("resource://services-sync/util.js");
-XPCOMUtils.defineLazyModuleGetter(this, "getRepairRequestor",
- "resource://services-sync/collection_repair.js");
-XPCOMUtils.defineLazyModuleGetter(this, "getAllRepairRequestors",
- "resource://services-sync/collection_repair.js");
-
-const log = Log.repository.getLogger("Sync.Doctor");
-
-this.REPAIR_ADVANCE_PERIOD = 86400; // 1 day
-
-this.Doctor = {
- anyClientsRepairing(service, collection, ignoreFlowID = null) {
- if (!service || !service.clientsEngine) {
- log.info("Missing clients engine, assuming we're in test code");
- return false;
- }
- return service.clientsEngine.remoteClients.some(client =>
- client.commands && client.commands.some(command => {
- if (command.command != "repairResponse" && command.command != "repairRequest") {
- return false;
- }
- if (!command.args || command.args.length != 1) {
- return false;
- }
- if (command.args[0].collection != collection) {
- return false;
- }
- if (ignoreFlowID != null && command.args[0].flowID == ignoreFlowID) {
- return false;
- }
- return true;
- })
- );
- },
-
- async consult(recentlySyncedEngines) {
- if (!Services.telemetry.canRecordBase) {
- log.info("Skipping consultation: telemetry reporting is disabled");
- return;
- }
-
- let engineInfos = this._getEnginesToValidate(recentlySyncedEngines);
-
- await this._runValidators(engineInfos);
-
- // We are called at the end of a sync, which is a good time to periodically
- // check each repairer to see if it can advance.
- if (this._now() - this.lastRepairAdvance > REPAIR_ADVANCE_PERIOD) {
- try {
- for (let [collection, requestor] of Object.entries(this._getAllRepairRequestors())) {
- try {
- let advanced = await requestor.continueRepairs();
- log.info(`${collection} reparier ${advanced ? "advanced" : "did not advance"}.`);
- } catch (ex) {
- if (Async.isShutdownException(ex)) {
- throw ex;
- }
- log.error(`${collection} repairer failed`, ex);
- }
- }
- } finally {
- this.lastRepairAdvance = Math.floor(this._now());
- }
- }
- },
-
- _getEnginesToValidate(recentlySyncedEngines) {
- let result = {};
- for (let e of recentlySyncedEngines) {
- let prefPrefix = `engine.${e.name}.`;
- if (!Svc.Prefs.get(prefPrefix + "validation.enabled", false)) {
- log.info(`Skipping check of ${e.name} - disabled via preferences`);
- continue;
- }
- // Check the last validation time for the engine.
- let lastValidation = Svc.Prefs.get(prefPrefix + "validation.lastTime", 0);
- let validationInterval = Svc.Prefs.get(prefPrefix + "validation.interval");
- let nowSeconds = this._now();
-
- if (nowSeconds - lastValidation < validationInterval) {
- log.info(`Skipping validation of ${e.name}: too recent since last validation attempt`);
- continue;
- }
- // Update the time now, even if we decline to actually perform a
- // validation. We don't want to check the rest of these more frequently
- // than once a day.
- Svc.Prefs.set(prefPrefix + "validation.lastTime", Math.floor(nowSeconds));
-
- // Validation only occurs a certain percentage of the time.
- let validationProbability = Svc.Prefs.get(prefPrefix + "validation.percentageChance", 0) / 100.0;
- if (validationProbability < Math.random()) {
- log.info(`Skipping validation of ${e.name}: Probability threshold not met`);
- continue;
- }
-
- let maxRecords = Svc.Prefs.get(prefPrefix + "validation.maxRecords");
- if (!maxRecords) {
- log.info(`Skipping validation of ${e.name}: No maxRecords specified`);
- continue;
- }
- // OK, so this is a candidate - the final decision will be based on the
- // number of records actually found.
- result[e.name] = { engine: e, maxRecords };
- }
- return result;
- },
-
- async _runValidators(engineInfos) {
- if (Object.keys(engineInfos).length == 0) {
- log.info("Skipping validation: no engines qualify");
- return;
- }
-
- if (Object.values(engineInfos).filter(i => i.maxRecords != -1).length != 0) {
- // at least some of the engines have maxRecord restrictions which require
- // us to ask the server for the counts.
- let countInfo = await this._fetchCollectionCounts();
- for (let [engineName, recordCount] of Object.entries(countInfo)) {
- if (engineName in engineInfos) {
- engineInfos[engineName].recordCount = recordCount;
- }
- }
- }
-
- for (let [engineName, { engine, maxRecords, recordCount }] of Object.entries(engineInfos)) {
- // maxRecords of -1 means "any number", so we can skip asking the server.
- // Used for tests.
- if (maxRecords >= 0 && recordCount > maxRecords) {
- log.debug(`Skipping validation for ${engineName} because ` +
- `the number of records (${recordCount}) is greater ` +
- `than the maximum allowed (${maxRecords}).`);
- continue;
- }
- let validator = engine.getValidator();
- if (!validator) {
- continue;
- }
-
- if (!await validator.canValidate()) {
- log.debug(`Skipping validation for ${engineName} because validator.canValidate() is false`);
- continue;
- }
-
- // Let's do it!
- Services.console.logStringMessage(
- `Sync is about to run a consistency check of ${engine.name}. This may be slow, and ` +
- `can be controlled using the pref "services.sync.${engine.name}.validation.enabled".\n` +
- `If you encounter any problems because of this, please file a bug.`);
-
- // Make a new flowID just incase we end up starting a repair.
- let flowID = Utils.makeGUID();
- try {
- // XXX - must get the flow ID to either the validator, or directly to
- // telemetry. I guess it's probably OK to always record a flowID even
- // if we don't end up repairing?
- log.info(`Running validator for ${engine.name}`);
- let result = await validator.validate(engine);
- Observers.notify("weave:engine:validate:finish", result, engine.name);
- // And see if we should repair.
- await this._maybeCure(engine, result, flowID);
- } catch (ex) {
- if (Async.isShutdownException(ex)) {
- throw ex;
- }
- log.error(`Failed to run validation on ${engine.name}!`, ex);
- Observers.notify("weave:engine:validate:error", ex, engine.name);
- // Keep validating -- there's no reason to think that a failure for one
- // validator would mean the others will fail.
- }
- }
- },
-
- async _maybeCure(engine, validationResults, flowID) {
- if (!this._shouldRepair(engine)) {
- log.info(`Skipping repair of ${engine.name} - disabled via preferences`);
- return;
- }
-
- let requestor = this._getRepairRequestor(engine.name);
- let didStart = false;
- if (requestor) {
- if (requestor.tryServerOnlyRepairs(validationResults)) {
- return; // TODO: It would be nice if we could request a validation to be
- // done on next sync.
- }
- didStart = await requestor.startRepairs(validationResults, flowID);
- }
- log.info(`${didStart ? "did" : "didn't"} start a repair of ${engine.name} with flowID ${flowID}`);
- },
-
- _shouldRepair(engine) {
- return Svc.Prefs.get(`engine.${engine.name}.repair.enabled`, false);
- },
-
- // mainly for mocking.
- async _fetchCollectionCounts() {
- let collectionCountsURL = Service.userBaseURL + "info/collection_counts";
- try {
- let infoResp = await Service._fetchInfo(collectionCountsURL);
- if (!infoResp.success) {
- log.error("Can't fetch collection counts: request to info/collection_counts responded with "
- + infoResp.status);
- return {};
- }
- return infoResp.obj; // might throw because obj is a getter which parses json.
- } catch (ex) {
- if (Async.isShutdownException(ex)) {
- throw ex;
- }
- // Not running validation is totally fine, so we just write an error log and return.
- log.error("Caught error when fetching counts", ex);
- return {};
- }
- },
-
- get lastRepairAdvance() {
- return Svc.Prefs.get("doctor.lastRepairAdvance", 0);
- },
-
- set lastRepairAdvance(value) {
- Svc.Prefs.set("doctor.lastRepairAdvance", value);
- },
-
- // functions used so tests can mock them
- _now() {
- // We use the server time, which is SECONDS
- return Resource.serverTime;
- },
-
- _getRepairRequestor(name) {
- return getRepairRequestor(name);
- },
-
- _getAllRepairRequestors() {
- return getAllRepairRequestors();
- }
-};
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -745,25 +745,16 @@ Engine.prototype = {
this._tracker.ignoreAll = false;
this._tracker.clearChangedIDs();
},
async wipeClient() {
return this._notify("wipe-client", this.name, this._wipeClient)();
},
- /**
- * If one exists, initialize and return a validator for this engine (which
- * must have a `validate(engine)` method that returns a promise to an object
- * with a getSummary method). Otherwise return null.
- */
- getValidator() {
- return null;
- },
-
async finalize() {
await this._tracker.finalize();
},
};
this.SyncEngine = function SyncEngine(name, service) {
Engine.call(this, name || "SyncEngine", service);
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -42,25 +42,24 @@ Cu.import("resource://gre/modules/XPCOMU
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-sync/addonsreconciler.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/collection_validator.js");
Cu.import("resource://services-common/async.js");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
"resource://gre/modules/addons/AddonRepository.jsm");
-this.EXPORTED_SYMBOLS = ["AddonsEngine", "AddonValidator"];
+this.EXPORTED_SYMBOLS = ["AddonsEngine"];
// 7 days in milliseconds.
const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
/**
* AddonRecord represents the state of an add-on in an application.
*
* Each add-on has its own record for each application ID it is installed
@@ -734,69 +733,8 @@ AddonsTracker.prototype = {
this.reconciler.addChangeListener(this);
},
stopTracking() {
this.reconciler.removeChangeListener(this);
this.reconciler.stopListening();
},
};
-
-class AddonValidator extends CollectionValidator {
- constructor(engine = null) {
- super("addons", "id", [
- "addonID",
- "enabled",
- "applicationID",
- "source"
- ]);
- this.engine = engine;
- }
-
- async getClientItems() {
- const installed = await AddonManager.getAllAddons();
- const addonsWithPendingOperation = await AddonManager.getAddonsWithOperationsByTypes(["extension", "theme"]);
- // Addons pending install won't be in the first list, but addons pending
- // uninstall/enable/disable will be in both lists.
- let all = new Map(installed.map(addon => [addon.id, addon]));
- for (let addon of addonsWithPendingOperation) {
- all.set(addon.id, addon);
- }
- // Convert to an array since Map.prototype.values returns an iterable
- return [...all.values()];
- }
-
- normalizeClientItem(item) {
- let enabled = !item.userDisabled;
- if (item.pendingOperations & AddonManager.PENDING_ENABLE) {
- enabled = true;
- } else if (item.pendingOperations & AddonManager.PENDING_DISABLE) {
- enabled = false;
- }
- return {
- enabled,
- id: item.syncGUID,
- addonID: item.id,
- applicationID: Services.appinfo.ID,
- source: "amo", // check item.foreignInstall?
- original: item
- };
- }
-
- async normalizeServerItem(item) {
- let guid = await this.engine._findDupe(item);
- if (guid) {
- item.id = guid;
- }
- return item;
- }
-
- clientUnderstands(item) {
- return item.applicationID === Services.appinfo.ID;
- }
-
- syncedByClient(item) {
- return !item.original.hidden &&
- !item.original.isSystem &&
- !(item.original.pendingOperations & AddonManager.PENDING_UNINSTALL) &&
- this.engine.isAddonSyncable(item.original, true);
- }
-}
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -13,18 +13,16 @@ var Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/util.js");
-XPCOMUtils.defineLazyModuleGetter(this, "BookmarkValidator",
- "resource://services-sync/bookmark_validator.js");
XPCOMUtils.defineLazyGetter(this, "PlacesBundle", () => {
let bundleService = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService);
return bundleService.createBundle("chrome://places/locale/places.properties");
});
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesSyncUtils",
@@ -625,20 +623,16 @@ BookmarksEngine.prototype = {
child => !newChildren.has(child)) : [];
// Some of the children in `order` might have been deleted, or moved to
// other folders. `PlacesSyncUtils.bookmarks.order` ignores them.
let order = newRecord.children ?
[...newRecord.children, ...missingChildren] : missingChildren;
this._log.debug("Recording children of " + localRecord.id, order);
this._store._childrenToOrder[localRecord.id] = order;
- },
-
- getValidator() {
- return new BookmarkValidator();
}
};
function BookmarksStore(name, engine) {
Store.call(this, name, engine);
this._itemsToDelete = new Set();
}
BookmarksStore.prototype = {
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -747,55 +747,47 @@ ClientEngine.prototype = {
case "displayURI":
let [uri, clientId, title] = args;
URIsToDisplay.push({ uri, clientId, title });
break;
case "repairResponse": {
// When we send a repair request to another device that understands
// it, that device will send a response indicating what it did.
let response = args[0];
- let requestor = getRepairRequestor(response.collection);
- if (!requestor) {
- this._log.warn("repairResponse for unknown collection", response);
- break;
- }
- if (!(await requestor.continueRepairs(response))) {
- this._log.warn("repairResponse couldn't continue the repair", response);
+ if (this.service.repairManager) {
+ await this.service.repairManager.handleRepairReponse(response, rawCommand);
}
break;
}
case "repairRequest": {
// Another device has sent us a request to make some repair.
let request = args[0];
- let responder = getRepairResponder(request.collection);
- if (!responder) {
- this._log.warn("repairRequest for unknown collection", request);
- break;
- }
- try {
- if ((await responder.repair(request, rawCommand))) {
- // We've started a repair - once that collection has synced it
- // will write a "response" command and arrange for this repair
- // request to be removed from the local command list - if we
- // removed it now we might fail to write a response in cases of
- // premature shutdown etc.
- shouldRemoveCommand = false;
+ if (this.service.repairManager) {
+ try {
+ if ((await this.service.repairManager.handleRepairRequest(request, rawCommand))) {
+ // We've started a repair - once that collection has synced it
+ // will write a "response" command and arrange for this repair
+ // request to be removed from the local command list - if we
+ // removed it now we might fail to write a response in cases of
+ // premature shutdown etc.
+ shouldRemoveCommand = false;
+ }
+ } catch (ex) {
+ if (Async.isShutdownException(ex)) {
+ // Let's assume this error was caused by the shutdown, so let
+ // it try again next time.
+ throw ex;
+ }
+ // otherwise there are no second chances - the command is removed
+ // and will not be tried again.
+ // (Note that this shouldn't be hit in the normal case - it's
+ // expected the responder will handle all reasonable failures and
+ // write a response indicating that it couldn't do what was asked.)
+ this._log.error("Failed to handle a repair request", ex);
}
- } catch (ex) {
- if (Async.isShutdownException(ex)) {
- // Let's assume this error was caused by the shutdown, so let
- // it try again next time.
- throw ex;
- }
- // otherwise there are no second chances - the command is removed
- // and will not be tried again.
- // (Note that this shouldn't be hit in the normal case - it's
- // expected the responder will handle all reasonable failures and
- // write a response indicating that it couldn't do what was asked.)
- this._log.error("Failed to handle a repair request", ex);
}
break;
}
default:
this._log.warn("Received an unknown command: " + command);
break;
}
// Add the command to the "cleared" commands list
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -1,24 +1,23 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-this.EXPORTED_SYMBOLS = ["FormEngine", "FormRec", "FormValidator"];
+this.EXPORTED_SYMBOLS = ["FormEngine", "FormRec"];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/collection_validator.js");
Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
"resource://gre/modules/FormHistory.jsm");
const FORMS_TTL = 3 * 365 * 24 * 60 * 60; // Three years in seconds.
this.FormRec = function FormRec(collection, id) {
@@ -112,17 +111,25 @@ FormEngine.prototype = {
syncPriority: 6,
get prefName() {
return "history";
},
async _findDupe(item) {
return FormWrapper.getGUID(item.name, item.value);
- }
+ },
+
+ // For the validator
+ _search(terms, searchData) {
+ return FormWrapper._search(terms, searchData);
+ },
+ _getGUID(name, value) {
+ return FormWrapper.getGUID(name, value);
+ },
};
function FormStore(name, engine) {
Store.call(this, name, engine);
}
FormStore.prototype = {
__proto__: Store.prototype,
@@ -246,61 +253,8 @@ FormTracker.prototype = {
trackEntry(guid) {
if (this.addChangedID(guid)) {
this.score += SCORE_INCREMENT_MEDIUM;
}
},
};
-
-class FormsProblemData extends CollectionProblemData {
- getSummary() {
- // We don't support syncing deleted form data, so "clientMissing" isn't a problem
- return super.getSummary().filter(entry =>
- entry.name !== "clientMissing");
- }
-}
-
-class FormValidator extends CollectionValidator {
- constructor() {
- super("forms", "id", ["name", "value"]);
- this.ignoresMissingClients = true;
- }
-
- emptyProblemData() {
- return new FormsProblemData();
- }
-
- async getClientItems() {
- return FormWrapper._search(["guid", "fieldname", "value"], {});
- }
-
- normalizeClientItem(item) {
- return {
- id: item.guid,
- guid: item.guid,
- name: item.fieldname,
- fieldname: item.fieldname,
- value: item.value,
- original: item,
- };
- }
-
- async normalizeServerItem(item) {
- let res = Object.assign({
- guid: item.id,
- fieldname: item.name,
- original: item,
- }, item);
- // Missing `name` or `value` causes the getGUID call to throw
- if (item.name !== undefined && item.value !== undefined) {
- let guid = await FormWrapper.getGUID(item.name, item.value);
- if (guid) {
- res.guid = guid;
- res.id = guid;
- res.duped = true;
- }
- }
-
- return res;
- }
-}
--- a/services/sync/modules/engines/passwords.js
+++ b/services/sync/modules/engines/passwords.js
@@ -1,20 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-this.EXPORTED_SYMBOLS = ["PasswordEngine", "LoginRec", "PasswordValidator"];
+this.EXPORTED_SYMBOLS = ["PasswordEngine", "LoginRec"];
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/collection_validator.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/async.js");
const SYNCABLE_LOGIN_FIELDS = [
// `nsILoginInfo` fields.
"hostname",
"formSubmitURL",
@@ -389,51 +388,8 @@ PasswordTracker.prototype = {
}
if (!this.addChangedID(login.guid)) {
return false;
}
this.score += SCORE_INCREMENT_XLARGE;
return true;
},
};
-
-class PasswordValidator extends CollectionValidator {
- constructor() {
- super("passwords", "id", [
- "hostname",
- "formSubmitURL",
- "httpRealm",
- "password",
- "passwordField",
- "username",
- "usernameField",
- ]);
- }
-
- getClientItems() {
- let logins = Services.logins.getAllLogins({});
- let syncHosts = Utils.getSyncCredentialsHosts();
- let result = logins.map(l => l.QueryInterface(Ci.nsILoginMetaInfo))
- .filter(l => !syncHosts.has(l.hostname));
- return Promise.resolve(result);
- }
-
- normalizeClientItem(item) {
- return {
- id: item.guid,
- guid: item.guid,
- hostname: item.hostname,
- formSubmitURL: item.formSubmitURL,
- httpRealm: item.httpRealm,
- password: item.password,
- passwordField: item.passwordField,
- username: item.username,
- usernameField: item.usernameField,
- original: item,
- };
- }
-
- async normalizeServerItem(item) {
- return Object.assign({ guid: item.id }, item);
- }
-}
-
-
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -155,16 +155,29 @@ Sync11Service.prototype = {
// Generate and cache various URLs under the storage API for this user
this.infoURL = this.userBaseURL + "info/collections";
this.storageURL = this.userBaseURL + "storage/";
this.metaURL = this.storageURL + "meta/global";
this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO;
},
+ // A reference to the sync-repair extension singleton. The extension takes care of
+ // attaching itself via setRepairManager() and clearRepairManager()
+ repairManager: null,
+ setRepairManager(manager) {
+ this._log.info("Manager for sync-repair registered with sync service");
+ this.repairManager = manager;
+ },
+
+ clearRepairManager() {
+ this._log.info("Manager for sync-repair disabled");
+ this.repairManager = null;
+ },
+
_checkCrypto: function _checkCrypto() {
let ok = false;
try {
let iv = Weave.Crypto.generateRandomIV();
if (iv.length == 24)
ok = true;
--- a/services/sync/modules/stages/enginesync.js
+++ b/services/sync/modules/stages/enginesync.js
@@ -10,18 +10,16 @@ this.EXPORTED_SYMBOLS = ["EngineSynchron
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-common/async.js");
-XPCOMUtils.defineLazyModuleGetter(this, "Doctor",
- "resource://services-sync/doctor.js");
/**
* Perform synchronization of engines.
*
* This was originally split out of service.js. The API needs lots of love.
*/
this.EngineSynchronizer = function EngineSynchronizer(service) {
this._log = Log.repository.getLogger("Sync.Synchronizer");
@@ -174,17 +172,19 @@ EngineSynchronizer.prototype = {
await this.service.uploadMetaGlobal(meta);
delete meta.isNew;
delete meta.changed;
} catch (error) {
this._log.error("Unable to upload meta/global. Leaving marked as new.");
}
}
- await Doctor.consult(enginesToValidate);
+ if (this.service.repairManager) {
+ await this.service.repairManager.consultDoctor(enginesToValidate);
+ }
// If there were no sync engine failures
if (this.service.status.service != SYNC_FAILED_PARTIAL) {
Svc.Prefs.set("lastSync", new Date().toString());
this.service.status.sync = SYNC_SUCCEEDED;
}
} finally {
Svc.Prefs.reset("firstSync");
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -14,23 +14,18 @@ XPCSHELL_TESTS_MANIFESTS += ['tests/unit
EXTRA_COMPONENTS += [
'SyncComponents.manifest',
'Weave.js',
]
EXTRA_JS_MODULES['services-sync'] += [
'modules/addonsreconciler.js',
'modules/addonutils.js',
- 'modules/bookmark_repair.js',
- 'modules/bookmark_validator.js',
'modules/browserid_identity.js',
- 'modules/collection_repair.js',
- 'modules/collection_validator.js',
'modules/constants.js',
- 'modules/doctor.js',
'modules/engines.js',
'modules/keys.js',
'modules/main.js',
'modules/policies.js',
'modules/record.js',
'modules/resource.js',
'modules/service.js',
'modules/status.js',
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -25,17 +25,21 @@ add_task(async function head_setup() {
await this.Service.promiseInitialized;
}
});
// ================================================
// Load mocking/stubbing library, sinon
// docs: http://sinonjs.org/releases/v2.3.2/
Cu.import("resource://gre/modules/Timer.jsm");
-Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js", this);
+XPCOMUtils.defineLazyGetter(this, "sinon", () => {
+ Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js", this)
+ return sinon;
+});
+
/* globals sinon */
// ================================================
XPCOMUtils.defineLazyGetter(this, "SyncPingSchema", function() {
let ns = {};
Cu.import("resource://gre/modules/FileUtils.jsm", ns);
Cu.import("resource://gre/modules/NetUtil.jsm", ns);
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
@@ -511,17 +515,22 @@ async function registerRotaryEngine() {
await Service.engineManager.register(RotaryEngine);
let engine = Service.engineManager.get("rotary");
engine.enabled = true;
return { engine, tracker: engine._tracker };
}
// Set the validation prefs to attempt validation every time to avoid non-determinism.
-function enableValidationPrefs() {
+function forceBookmarkValidation() {
+ if (!Service.repairManager) {
+ let ns = {};
+ Cu.import("resource://sync-repair/repair_manager.jsm", ns);
+ Service.setRepairManager(new ns.RepairManager());
+ }
Svc.Prefs.set("engine.bookmarks.validation.interval", 0);
Svc.Prefs.set("engine.bookmarks.validation.percentageChance", 100);
Svc.Prefs.set("engine.bookmarks.validation.maxRecords", -1);
Svc.Prefs.set("engine.bookmarks.validation.enabled", true);
}
async function serverForEnginesWithKeys(users, engines, callback) {
// Generate and store a fake default key bundle to avoid resetting the client
--- a/services/sync/tests/unit/test_bookmark_duping.js
+++ b/services/sync/tests/unit/test_bookmark_duping.js
@@ -3,17 +3,19 @@
Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
-Cu.import("resource://services-sync/bookmark_validator.js");
+// XXX - This... doesn't work. The loadSubscript dance doesn't seem
+// to either, is there a good way to do this?
+Cu.import("resource://sync-repair/bookmark_validator.jsm");
const bms = PlacesUtils.bookmarks;
add_task(async function setup() {
initTestLogging("Trace");
await Service.engineManager.unregister("bookmarks");
});
deleted file mode 100644
--- a/services/sync/tests/unit/test_bookmark_repair.js
+++ /dev/null
@@ -1,516 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Tests the bookmark repair requestor and responder end-to-end (ie, without
-// many mocks)
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://gre/modules/osfile.jsm");
-Cu.import("resource://services-sync/bookmark_repair.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/doctor.js");
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/engines/clients.js");
-Cu.import("resource://services-sync/engines/bookmarks.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-
-const LAST_BOOKMARK_SYNC_PREFS = [
- "bookmarks.lastSync",
- "bookmarks.lastSyncLocal",
-];
-
-const BOOKMARK_REPAIR_STATE_PREFS = [
- "client.GUID",
- "doctor.lastRepairAdvance",
- ...LAST_BOOKMARK_SYNC_PREFS,
- ...Object.values(BookmarkRepairRequestor.PREF).map(name =>
- `repairs.bookmarks.${name}`
- ),
-];
-
-let clientsEngine;
-let bookmarksEngine;
-var recordedEvents = [];
-
-add_task(async function setup() {
- clientsEngine = Service.clientsEngine;
- clientsEngine.ignoreLastModifiedOnProcessCommands = true;
- bookmarksEngine = Service.engineManager.get("bookmarks");
-
- await generateNewKeys(Service.collectionKeys);
-
- Service.recordTelemetryEvent = (object, method, value, extra = undefined) => {
- recordedEvents.push({ object, method, value, extra });
- };
-
- initTestLogging("Trace");
- Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
- Log.repository.getLogger("Sync.Engine.Clients").level = Log.Level.Trace;
- Log.repository.getLogger("Sqlite").level = Log.Level.Info; // less noisy
-});
-
-function checkRecordedEvents(expected, message) {
- deepEqual(recordedEvents, expected, message);
- // and clear the list so future checks are easier to write.
- recordedEvents = [];
-}
-
-// Backs up and resets all preferences to their default values. Returns a
-// function that restores the preferences when called.
-function backupPrefs(names) {
- let state = new Map();
- for (let name of names) {
- state.set(name, Svc.Prefs.get(name));
- Svc.Prefs.reset(name);
- }
- return () => {
- for (let [name, value] of state) {
- Svc.Prefs.set(name, value);
- }
- };
-}
-
-async function promiseValidationDone(expected) {
- // wait for a validation to complete.
- let obs = promiseOneObserver("weave:engine:validate:finish");
- let { subject: validationResult } = await obs;
- // check the results - anything non-zero is checked against |expected|
- let summary = validationResult.problems.getSummary();
- let actual = summary.filter(({name, count}) => count);
- actual.sort((a, b) => String(a.name).localeCompare(b.name));
- expected.sort((a, b) => String(a.name).localeCompare(b.name));
- deepEqual(actual, expected);
-}
-
-async function cleanup(server) {
- await bookmarksEngine._store.wipe();
- await clientsEngine._store.wipe();
- Svc.Prefs.resetBranch("");
- Service.recordManager.clearCache();
- await promiseStopServer(server);
-}
-
-add_task(async function test_bookmark_repair_integration() {
- enableValidationPrefs();
-
- _("Ensure that a validation error triggers a repair request.");
-
- let server = await serverForFoo(bookmarksEngine);
- await SyncTestingInfrastructure(server);
-
- let user = server.user("foo");
-
- let initialID = Service.clientsEngine.localID;
- let remoteID = Utils.makeGUID();
- try {
-
- _("Syncing to initialize crypto etc.");
- await Service.sync();
-
- _("Create remote client record");
- server.insertWBO("foo", "clients", new ServerWBO(remoteID, encryptPayload({
- id: remoteID,
- name: "Remote client",
- type: "desktop",
- commands: [],
- version: "54",
- protocols: ["1.5"],
- }), Date.now() / 1000));
-
- _("Create bookmark and folder");
- let folderInfo = await PlacesUtils.bookmarks.insert({
- parentGuid: PlacesUtils.bookmarks.toolbarGuid,
- type: PlacesUtils.bookmarks.TYPE_FOLDER,
- title: "Folder 1",
- });
- let bookmarkInfo = await PlacesUtils.bookmarks.insert({
- parentGuid: folderInfo.guid,
- url: "http://getfirefox.com/",
- title: "Get Firefox!",
- });
-
- _(`Upload ${folderInfo.guid} and ${bookmarkInfo.guid} to server`);
- let validationPromise = promiseValidationDone([]);
- await Service.sync();
- equal(clientsEngine.stats.numClients, 2, "Clients collection should have 2 records");
- await validationPromise;
- checkRecordedEvents([], "Should not start repair after first sync");
-
- _("Back up last sync timestamps for remote client");
- let restoreRemoteLastBookmarkSync = backupPrefs(LAST_BOOKMARK_SYNC_PREFS);
-
- _(`Delete ${bookmarkInfo.guid} locally and on server`);
- // Now we will reach into the server and hard-delete the bookmark
- user.collection("bookmarks").remove(bookmarkInfo.guid);
- // And delete the bookmark, but cheat by telling places that Sync did
- // it, so we don't end up with a tombstone.
- await PlacesUtils.bookmarks.remove(bookmarkInfo.guid, {
- source: PlacesUtils.bookmarks.SOURCE_SYNC,
- });
- deepEqual((await bookmarksEngine.pullNewChanges()), {},
- `Should not upload tombstone for ${bookmarkInfo.guid}`);
-
- // sync again - we should have a few problems...
- _("Sync again to trigger repair");
- validationPromise = promiseValidationDone([
- {"name": "missingChildren", "count": 1},
- {"name": "structuralDifferences", "count": 1},
- ]);
- await Service.sync();
- await validationPromise;
- let flowID = Svc.Prefs.get("repairs.bookmarks.flowID");
- checkRecordedEvents([{
- object: "repair",
- method: "started",
- value: undefined,
- extra: {
- flowID,
- numIDs: "2",
- },
- }, {
- object: "sendcommand",
- method: "repairRequest",
- value: undefined,
- extra: {
- flowID,
- deviceID: Service.identity.hashedDeviceID(remoteID),
- },
- }, {
- object: "repair",
- method: "request",
- value: "upload",
- extra: {
- deviceID: Service.identity.hashedDeviceID(remoteID),
- flowID,
- numIDs: "2",
- },
- }], "Should record telemetry events for repair request");
-
- // We should have started a repair with our second client.
- equal((await clientsEngine.getClientCommands(remoteID)).length, 1,
- "Should queue repair request for remote client after repair");
- _("Sync to send outgoing repair request");
- await Service.sync();
- equal((await clientsEngine.getClientCommands(remoteID)).length, 0,
- "Should send repair request to remote client after next sync");
- checkRecordedEvents([],
- "Should not record repair telemetry after sending repair request");
-
- _("Back up repair state to restore later");
- let restoreInitialRepairState = backupPrefs(BOOKMARK_REPAIR_STATE_PREFS);
-
- // so now let's take over the role of that other client!
- _("Create new clients engine pretending to be remote client");
- let remoteClientsEngine = Service.clientsEngine = new ClientEngine(Service);
- remoteClientsEngine.ignoreLastModifiedOnProcessCommands = true;
- await remoteClientsEngine.initialize();
- remoteClientsEngine.localID = remoteID;
-
- _("Restore missing bookmark");
- // Pretend Sync wrote the bookmark, so that we upload it as part of the
- // repair instead of the sync.
- bookmarkInfo.source = PlacesUtils.bookmarks.SOURCE_SYNC;
- await PlacesUtils.bookmarks.insert(bookmarkInfo);
- restoreRemoteLastBookmarkSync();
-
- _("Sync as remote client");
- await Service.sync();
- checkRecordedEvents([{
- object: "processcommand",
- method: "repairRequest",
- value: undefined,
- extra: {
- flowID,
- },
- }, {
- object: "repairResponse",
- method: "uploading",
- value: undefined,
- extra: {
- flowID,
- numIDs: "2",
- },
- }, {
- object: "sendcommand",
- method: "repairResponse",
- value: undefined,
- extra: {
- flowID,
- deviceID: Service.identity.hashedDeviceID(initialID),
- },
- }, {
- object: "repairResponse",
- method: "finished",
- value: undefined,
- extra: {
- flowID,
- numIDs: "2",
- }
- }], "Should record telemetry events for repair response");
-
- // We should queue the repair response for the initial client.
- equal((await remoteClientsEngine.getClientCommands(initialID)).length, 1,
- "Should queue repair response for initial client after repair");
- ok(user.collection("bookmarks").wbo(bookmarkInfo.guid),
- "Should upload missing bookmark");
-
- _("Sync to upload bookmark and send outgoing repair response");
- await Service.sync();
- equal((await remoteClientsEngine.getClientCommands(initialID)).length, 0,
- "Should send repair response to initial client after next sync");
- checkRecordedEvents([],
- "Should not record repair telemetry after sending repair response");
- ok(!Services.prefs.prefHasUserValue("services.sync.repairs.bookmarks.state"),
- "Remote client should not be repairing");
-
- _("Pretend to be initial client again");
- Service.clientsEngine = clientsEngine;
-
- _("Restore incomplete Places database and prefs");
- await PlacesUtils.bookmarks.remove(bookmarkInfo.guid, {
- source: PlacesUtils.bookmarks.SOURCE_SYNC,
- });
- restoreInitialRepairState();
- ok(Services.prefs.prefHasUserValue("services.sync.repairs.bookmarks.state"),
- "Initial client should still be repairing");
-
- _("Sync as initial client");
- let revalidationPromise = promiseValidationDone([]);
- await Service.sync();
- let restoredBookmarkInfo = await PlacesUtils.bookmarks.fetch(bookmarkInfo.guid);
- ok(restoredBookmarkInfo, "Missing bookmark should be downloaded to initial client");
- checkRecordedEvents([{
- object: "processcommand",
- method: "repairResponse",
- value: undefined,
- extra: {
- flowID,
- },
- }, {
- object: "repair",
- method: "response",
- value: "upload",
- extra: {
- flowID,
- deviceID: Service.identity.hashedDeviceID(remoteID),
- numIDs: "2",
- },
- }, {
- object: "repair",
- method: "finished",
- value: undefined,
- extra: {
- flowID,
- numIDs: "0",
- },
- }]);
- await revalidationPromise;
- ok(!Services.prefs.prefHasUserValue("services.sync.repairs.bookmarks.state"),
- "Should clear repair pref after successfully completing repair");
- } finally {
- await cleanup(server);
- clientsEngine = Service.clientsEngine = new ClientEngine(Service);
- clientsEngine.ignoreLastModifiedOnProcessCommands = true;
- clientsEngine.initialize();
- }
-});
-
-add_task(async function test_repair_client_missing() {
- enableValidationPrefs();
-
- _("Ensure that a record missing from the client only will get re-downloaded from the server");
-
- let server = await serverForFoo(bookmarksEngine);
- await SyncTestingInfrastructure(server);
-
- let remoteID = Utils.makeGUID();
- try {
-
- _("Syncing to initialize crypto etc.");
- await Service.sync();
-
- _("Create remote client record");
- server.insertWBO("foo", "clients", new ServerWBO(remoteID, encryptPayload({
- id: remoteID,
- name: "Remote client",
- type: "desktop",
- commands: [],
- version: "54",
- protocols: ["1.5"],
- }), Date.now() / 1000));
-
- let bookmarkInfo = await PlacesUtils.bookmarks.insert({
- parentGuid: PlacesUtils.bookmarks.toolbarGuid,
- url: "http://getfirefox.com/",
- title: "Get Firefox!",
- });
-
- let validationPromise = promiseValidationDone([]);
- _("Syncing.");
- await Service.sync();
- // should have 2 clients
- equal(clientsEngine.stats.numClients, 2);
- await validationPromise;
-
- // Delete the bookmark localy, but cheat by telling places that Sync did
- // it, so Sync still thinks we have it.
- await PlacesUtils.bookmarks.remove(bookmarkInfo.guid, {
- source: PlacesUtils.bookmarks.SOURCE_SYNC,
- });
- // sanity check we aren't going to sync this removal.
- do_check_empty((await bookmarksEngine.pullNewChanges()));
- // sanity check that the bookmark is not there anymore
- do_check_false(await PlacesUtils.bookmarks.fetch(bookmarkInfo.guid));
-
- // sync again - we should have a few problems...
- _("Syncing again.");
- validationPromise = promiseValidationDone([
- {"name": "clientMissing", "count": 1},
- {"name": "structuralDifferences", "count": 1},
- ]);
- await Service.sync();
- await validationPromise;
-
- // We shouldn't have started a repair with our second client.
- equal((await clientsEngine.getClientCommands(remoteID)).length, 0);
-
- // Trigger a sync (will request the missing item)
- await Service.sync();
-
- // And we got our bookmark back
- do_check_true(await PlacesUtils.bookmarks.fetch(bookmarkInfo.guid));
- } finally {
- await cleanup(server);
- }
-});
-
-add_task(async function test_repair_server_missing() {
- enableValidationPrefs();
-
- _("Ensure that a record missing from the server only will get re-upload from the client");
-
- let server = await serverForFoo(bookmarksEngine);
- await SyncTestingInfrastructure(server);
-
- let user = server.user("foo");
-
- let remoteID = Utils.makeGUID();
- try {
-
- _("Syncing to initialize crypto etc.");
- await Service.sync();
-
- _("Create remote client record");
- server.insertWBO("foo", "clients", new ServerWBO(remoteID, encryptPayload({
- id: remoteID,
- name: "Remote client",
- type: "desktop",
- commands: [],
- version: "54",
- protocols: ["1.5"],
- }), Date.now() / 1000));
-
- let bookmarkInfo = await PlacesUtils.bookmarks.insert({
- parentGuid: PlacesUtils.bookmarks.toolbarGuid,
- url: "http://getfirefox.com/",
- title: "Get Firefox!",
- });
-
- let validationPromise = promiseValidationDone([]);
- _("Syncing.");
- await Service.sync();
- // should have 2 clients
- equal(clientsEngine.stats.numClients, 2);
- await validationPromise;
-
- // Now we will reach into the server and hard-delete the bookmark
- user.collection("bookmarks").wbo(bookmarkInfo.guid).delete();
-
- // sync again - we should have a few problems...
- _("Syncing again.");
- validationPromise = promiseValidationDone([
- {"name": "serverMissing", "count": 1},
- {"name": "missingChildren", "count": 1},
- ]);
- await Service.sync();
- await validationPromise;
-
- // We shouldn't have started a repair with our second client.
- equal((await clientsEngine.getClientCommands(remoteID)).length, 0);
-
- // Trigger a sync (will upload the missing item)
- await Service.sync();
-
- // And the server got our bookmark back
- do_check_true(user.collection("bookmarks").wbo(bookmarkInfo.guid));
- } finally {
- await cleanup(server);
- }
-});
-
-add_task(async function test_repair_server_deleted() {
- enableValidationPrefs();
-
- _("Ensure that a record marked as deleted on the server but present on the client will get deleted on the client");
-
- let server = await serverForFoo(bookmarksEngine);
- await SyncTestingInfrastructure(server);
-
- let remoteID = Utils.makeGUID();
- try {
-
- _("Syncing to initialize crypto etc.");
- await Service.sync();
-
- _("Create remote client record");
- server.insertWBO("foo", "clients", new ServerWBO(remoteID, encryptPayload({
- id: remoteID,
- name: "Remote client",
- type: "desktop",
- commands: [],
- version: "54",
- protocols: ["1.5"],
- }), Date.now() / 1000));
-
- let bookmarkInfo = await PlacesUtils.bookmarks.insert({
- parentGuid: PlacesUtils.bookmarks.toolbarGuid,
- url: "http://getfirefox.com/",
- title: "Get Firefox!",
- });
-
- let validationPromise = promiseValidationDone([]);
- _("Syncing.");
- await Service.sync();
- // should have 2 clients
- equal(clientsEngine.stats.numClients, 2);
- await validationPromise;
-
- // Now we will reach into the server and create a tombstone for that bookmark
- // but with a last-modified in the past - this way our sync isn't going to
- // pick up the record.
- server.insertWBO("foo", "bookmarks", new ServerWBO(bookmarkInfo.guid, encryptPayload({
- id: bookmarkInfo.guid,
- deleted: true,
- }), (Date.now() - 60000) / 1000));
-
- // sync again - we should have a few problems...
- _("Syncing again.");
- validationPromise = promiseValidationDone([
- {"name": "serverDeleted", "count": 1},
- {"name": "deletedChildren", "count": 1},
- {"name": "orphans", "count": 1}
- ]);
- await Service.sync();
- await validationPromise;
-
- // We shouldn't have started a repair with our second client.
- equal((await clientsEngine.getClientCommands(remoteID)).length, 0);
-
- // Trigger a sync (will upload the missing item)
- await Service.sync();
-
- // And the client deleted our bookmark
- do_check_true(!(await PlacesUtils.bookmarks.fetch(bookmarkInfo.guid)));
- } finally {
- await cleanup(server);
- }
-});
deleted file mode 100644
--- a/services/sync/tests/unit/test_bookmark_repair_requestor.js
+++ /dev/null
@@ -1,514 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-Cu.import("resource://services-sync/bookmark_repair.js");
-
-initTestLogging("Trace");
-
-function makeClientRecord(id, fields = {}) {
- return {
- id,
- version: fields.version || "54.0a1",
- type: fields.type || "desktop",
- stale: fields.stale || false,
- serverLastModified: fields.serverLastModified || 0,
- };
-}
-
-class MockClientsEngine {
- constructor(clientList) {
- this._clientList = clientList;
- this._sentCommands = {};
- }
-
- get remoteClients() {
- return Object.values(this._clientList);
- }
-
- remoteClient(id) {
- return this._clientList[id];
- }
-
- async sendCommand(command, args, clientID) {
- let cc = this._sentCommands[clientID] || [];
- cc.push({ command, args });
- this._sentCommands[clientID] = cc;
- }
-
- async getClientCommands(clientID) {
- return this._sentCommands[clientID] || [];
- }
-}
-
-class MockIdentity {
- hashedDeviceID(did) {
- return did; // don't hash it to make testing easier.
- }
-}
-
-class MockService {
- constructor(clientList) {
- this.clientsEngine = new MockClientsEngine(clientList);
- this.identity = new MockIdentity();
- this._recordedEvents = [];
- }
-
- recordTelemetryEvent(object, method, value, extra = undefined) {
- this._recordedEvents.push({ method, object, value, extra });
- }
-}
-
-function checkState(expected) {
- equal(Services.prefs.getCharPref("services.sync.repairs.bookmarks.state"), expected);
-}
-
-function checkRepairFinished() {
- try {
- let state = Services.prefs.getCharPref("services.sync.repairs.bookmarks.state");
- ok(false, state);
- } catch (ex) {
- ok(true, "no repair preference exists");
- }
-}
-
-function checkOutgoingCommand(service, clientID) {
- let sent = service.clientsEngine._sentCommands;
- deepEqual(Object.keys(sent), [clientID]);
- equal(sent[clientID].length, 1);
- equal(sent[clientID][0].command, "repairRequest");
-}
-
-function NewBookmarkRepairRequestor(mockService) {
- let req = new BookmarkRepairRequestor(mockService);
- req._now = () => Date.now() / 1000; // _now() is seconds.
- return req;
-}
-
-add_task(async function test_requestor_no_clients() {
- let mockService = new MockService({ });
- let requestor = NewBookmarkRepairRequestor(mockService);
- let validationInfo = {
- problems: {
- missingChildren: [
- {parent: "x", child: "a"},
- {parent: "x", child: "b"},
- {parent: "x", child: "c"}
- ],
- orphans: [],
- }
- };
- let flowID = Utils.makeGUID();
-
- await requestor.startRepairs(validationInfo, flowID);
- // there are no clients, so we should end up in "finished" (which we need to
- // check via telemetry)
- deepEqual(mockService._recordedEvents, [
- { object: "repair",
- method: "started",
- value: undefined,
- extra: { flowID, numIDs: 4 },
- },
- { object: "repair",
- method: "finished",
- value: undefined,
- extra: { flowID, numIDs: 4 },
- }
- ]);
-});
-
-add_task(async function test_requestor_one_client_no_response() {
- let mockService = new MockService({ "client-a": makeClientRecord("client-a") });
- let requestor = NewBookmarkRepairRequestor(mockService);
- let validationInfo = {
- problems: {
- missingChildren: [
- {parent: "x", child: "a"},
- {parent: "x", child: "b"},
- {parent: "x", child: "c"}
- ],
- orphans: [],
- }
- };
- let flowID = Utils.makeGUID();
- await requestor.startRepairs(validationInfo, flowID);
- // the command should now be outgoing.
- checkOutgoingCommand(mockService, "client-a");
-
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
- // asking it to continue stays in that state until we timeout or the command
- // is removed.
- await requestor.continueRepairs();
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
-
- // now pretend that client synced.
- mockService.clientsEngine._sentCommands = {};
- await requestor.continueRepairs();
- checkState(BookmarkRepairRequestor.STATE.SENT_SECOND_REQUEST);
- // the command should be outgoing again.
- checkOutgoingCommand(mockService, "client-a");
-
- // pretend that client synced again without writing a command.
- mockService.clientsEngine._sentCommands = {};
- await requestor.continueRepairs();
- // There are no more clients, so we've given up.
-
- checkRepairFinished();
- deepEqual(mockService._recordedEvents, [
- { object: "repair",
- method: "started",
- value: undefined,
- extra: { flowID, numIDs: 4 },
- },
- { object: "repair",
- method: "request",
- value: "upload",
- extra: { flowID, numIDs: 4, deviceID: "client-a" },
- },
- { object: "repair",
- method: "request",
- value: "upload",
- extra: { flowID, numIDs: 4, deviceID: "client-a" },
- },
- { object: "repair",
- method: "finished",
- value: undefined,
- extra: { flowID, numIDs: 4 },
- }
- ]);
-});
-
-add_task(async function test_requestor_one_client_no_sync() {
- let mockService = new MockService({ "client-a": makeClientRecord("client-a") });
- let requestor = NewBookmarkRepairRequestor(mockService);
- let validationInfo = {
- problems: {
- missingChildren: [
- {parent: "x", child: "a"},
- {parent: "x", child: "b"},
- {parent: "x", child: "c"}
- ],
- orphans: [],
- }
- };
- let flowID = Utils.makeGUID();
- await requestor.startRepairs(validationInfo, flowID);
- // the command should now be outgoing.
- checkOutgoingCommand(mockService, "client-a");
-
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
-
- // pretend we are now in the future.
- let theFuture = Date.now() + 300000000;
- requestor._now = () => theFuture;
-
- await requestor.continueRepairs();
-
- // We should be finished as we gave up in disgust.
- checkRepairFinished();
- deepEqual(mockService._recordedEvents, [
- { object: "repair",
- method: "started",
- value: undefined,
- extra: { flowID, numIDs: 4 },
- },
- { object: "repair",
- method: "request",
- value: "upload",
- extra: { flowID, numIDs: 4, deviceID: "client-a" },
- },
- { object: "repair",
- method: "abandon",
- value: "silent",
- extra: { flowID, deviceID: "client-a" },
- },
- { object: "repair",
- method: "finished",
- value: undefined,
- extra: { flowID, numIDs: 4 },
- }
- ]);
-});
-
-add_task(async function test_requestor_latest_client_used() {
- let mockService = new MockService({
- "client-early": makeClientRecord("client-early", { serverLastModified: Date.now() - 10 }),
- "client-late": makeClientRecord("client-late", { serverLastModified: Date.now() }),
- });
- let requestor = NewBookmarkRepairRequestor(mockService);
- let validationInfo = {
- problems: {
- missingChildren: [
- { parent: "x", child: "a" },
- ],
- orphans: [],
- }
- };
- await requestor.startRepairs(validationInfo, Utils.makeGUID());
- // the repair command should be outgoing to the most-recent client.
- checkOutgoingCommand(mockService, "client-late");
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
- // and this test is done - reset the repair.
- requestor.prefs.resetBranch();
-});
-
-add_task(async function test_requestor_client_vanishes() {
- let mockService = new MockService({
- "client-a": makeClientRecord("client-a"),
- "client-b": makeClientRecord("client-b"),
- });
- let requestor = NewBookmarkRepairRequestor(mockService);
- let validationInfo = {
- problems: {
- missingChildren: [
- {parent: "x", child: "a"},
- {parent: "x", child: "b"},
- {parent: "x", child: "c"}
- ],
- orphans: [],
- }
- };
- let flowID = Utils.makeGUID();
- await requestor.startRepairs(validationInfo, flowID);
- // the command should now be outgoing.
- checkOutgoingCommand(mockService, "client-a");
-
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
-
- mockService.clientsEngine._sentCommands = {};
- // Now let's pretend the client vanished.
- delete mockService.clientsEngine._clientList["client-a"];
-
- await requestor.continueRepairs();
- // We should have moved on to client-b.
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
- checkOutgoingCommand(mockService, "client-b");
-
- // Now let's pretend client B wrote all missing IDs.
- let response = {
- collection: "bookmarks",
- request: "upload",
- flowID: requestor._flowID,
- clientID: "client-b",
- ids: ["a", "b", "c", "x"],
- };
- await requestor.continueRepairs(response);
-
- // We should be finished as we got all our IDs.
- checkRepairFinished();
- deepEqual(mockService._recordedEvents, [
- { object: "repair",
- method: "started",
- value: undefined,
- extra: { flowID, numIDs: 4 },
- },
- { object: "repair",
- method: "request",
- value: "upload",
- extra: { flowID, numIDs: 4, deviceID: "client-a" },
- },
- { object: "repair",
- method: "abandon",
- value: "missing",
- extra: { flowID, deviceID: "client-a" },
- },
- { object: "repair",
- method: "request",
- value: "upload",
- extra: { flowID, numIDs: 4, deviceID: "client-b" },
- },
- { object: "repair",
- method: "response",
- value: "upload",
- extra: { flowID, deviceID: "client-b", numIDs: 4 },
- },
- { object: "repair",
- method: "finished",
- value: undefined,
- extra: { flowID, numIDs: 0 },
- }
- ]);
-});
-
-add_task(async function test_requestor_success_responses() {
- let mockService = new MockService({
- "client-a": makeClientRecord("client-a"),
- "client-b": makeClientRecord("client-b"),
- });
- let requestor = NewBookmarkRepairRequestor(mockService);
- let validationInfo = {
- problems: {
- missingChildren: [
- {parent: "x", child: "a"},
- {parent: "x", child: "b"},
- {parent: "x", child: "c"}
- ],
- orphans: [],
- }
- };
- let flowID = Utils.makeGUID();
- await requestor.startRepairs(validationInfo, flowID);
- // the command should now be outgoing.
- checkOutgoingCommand(mockService, "client-a");
-
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
-
- mockService.clientsEngine._sentCommands = {};
- // Now let's pretend the client wrote a response.
- let response = {
- collection: "bookmarks",
- request: "upload",
- clientID: "client-a",
- flowID: requestor._flowID,
- ids: ["a", "b"],
- };
- await requestor.continueRepairs(response);
- // We should have moved on to client 2.
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
- checkOutgoingCommand(mockService, "client-b");
-
- // Now let's pretend client B write the missing ID.
- response = {
- collection: "bookmarks",
- request: "upload",
- clientID: "client-b",
- flowID: requestor._flowID,
- ids: ["c", "x"],
- };
- await requestor.continueRepairs(response);
-
- // We should be finished as we got all our IDs.
- checkRepairFinished();
- deepEqual(mockService._recordedEvents, [
- { object: "repair",
- method: "started",
- value: undefined,
- extra: { flowID, numIDs: 4 },
- },
- { object: "repair",
- method: "request",
- value: "upload",
- extra: { flowID, numIDs: 4, deviceID: "client-a" },
- },
- { object: "repair",
- method: "response",
- value: "upload",
- extra: { flowID, deviceID: "client-a", numIDs: 2 },
- },
- { object: "repair",
- method: "request",
- value: "upload",
- extra: { flowID, numIDs: 2, deviceID: "client-b" },
- },
- { object: "repair",
- method: "response",
- value: "upload",
- extra: { flowID, deviceID: "client-b", numIDs: 2 },
- },
- { object: "repair",
- method: "finished",
- value: undefined,
- extra: { flowID, numIDs: 0 },
- }
- ]);
-});
-
-add_task(async function test_client_suitability() {
- let mockService = new MockService({
- "client-a": makeClientRecord("client-a"),
- "client-b": makeClientRecord("client-b", { type: "mobile" }),
- "client-c": makeClientRecord("client-c", { version: "52.0a1" }),
- "client-d": makeClientRecord("client-c", { version: "54.0a1" }),
- });
- let requestor = NewBookmarkRepairRequestor(mockService);
- ok(requestor._isSuitableClient(mockService.clientsEngine.remoteClient("client-a")));
- ok(!requestor._isSuitableClient(mockService.clientsEngine.remoteClient("client-b")));
- ok(!requestor._isSuitableClient(mockService.clientsEngine.remoteClient("client-c")));
- ok(requestor._isSuitableClient(mockService.clientsEngine.remoteClient("client-d")));
-});
-
-add_task(async function test_requestor_already_repairing_at_start() {
- let mockService = new MockService({ });
- let requestor = NewBookmarkRepairRequestor(mockService);
- requestor.anyClientsRepairing = () => true;
- let validationInfo = {
- problems: {
- missingChildren: [
- {parent: "x", child: "a"},
- {parent: "x", child: "b"},
- {parent: "x", child: "c"}
- ],
- orphans: [],
- }
- };
- let flowID = Utils.makeGUID();
-
- ok(!(await requestor.startRepairs(validationInfo, flowID)),
- "Shouldn't start repairs");
- equal(mockService._recordedEvents.length, 1);
- equal(mockService._recordedEvents[0].method, "aborted");
-});
-
-add_task(async function test_requestor_already_repairing_continue() {
- let clientB = makeClientRecord("client-b");
- let mockService = new MockService({
- "client-a": makeClientRecord("client-a"),
- "client-b": clientB
- });
- let requestor = NewBookmarkRepairRequestor(mockService);
- let validationInfo = {
- problems: {
- missingChildren: [
- {parent: "x", child: "a"},
- {parent: "x", child: "b"},
- {parent: "x", child: "c"}
- ],
- orphans: [],
- }
- };
- let flowID = Utils.makeGUID();
- await requestor.startRepairs(validationInfo, flowID);
- // the command should now be outgoing.
- checkOutgoingCommand(mockService, "client-a");
-
- checkState(BookmarkRepairRequestor.STATE.SENT_REQUEST);
- mockService.clientsEngine._sentCommands = {};
-
- // Now let's pretend the client wrote a response (it doesn't matter what's in here)
- let response = {
- collection: "bookmarks",
- request: "upload",
- clientID: "client-a",
- flowID: requestor._flowID,
- ids: ["a", "b"],
- };
-
- // and another client also started a request
- clientB.commands = [{
- args: [{ collection: "bookmarks", flowID: "asdf" }],
- command: "repairRequest",
- }];
-
-
- await requestor.continueRepairs(response);
-
- // We should have aborted now
- checkRepairFinished();
- const expected = [
- { method: "started",
- object: "repair",
- value: undefined,
- extra: { flowID, numIDs: "4" },
- },
- { method: "request",
- object: "repair",
- value: "upload",
- extra: { flowID, numIDs: "4", deviceID: "client-a" },
- },
- { method: "aborted",
- object: "repair",
- value: undefined,
- extra: { flowID, numIDs: "4", reason: "other clients repairing" },
- }
- ];
-
- deepEqual(mockService._recordedEvents, expected);
-});
deleted file mode 100644
--- a/services/sync/tests/unit/test_bookmark_repair_responder.js
+++ /dev/null
@@ -1,611 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource:///modules/PlacesUIUtils.jsm");
-Cu.import("resource://gre/modules/Log.jsm");
-
-Cu.import("resource://services-sync/engines/bookmarks.js");
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/bookmark_repair.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-
-initTestLogging("Trace");
-Log.repository.getLogger("Sync.Engine.Bookmarks").level = Log.Level.Trace;
-
-// Disable validation so that we don't try to automatically repair the server
-// when we sync.
-Svc.Prefs.set("engine.bookmarks.validation.enabled", false);
-
-// stub telemetry so we can easily check the right things are recorded.
-var recordedEvents = [];
-
-function checkRecordedEvents(expected) {
- deepEqual(recordedEvents, expected);
- // and clear the list so future checks are easier to write.
- recordedEvents = [];
-}
-
-function getServerBookmarks(server) {
- return server.user("foo").collection("bookmarks");
-}
-
-async function makeServer() {
- let server = await serverForFoo(bookmarksEngine);
- await SyncTestingInfrastructure(server);
- return server;
-}
-
-async function cleanup(server) {
- await promiseStopServer(server);
- await PlacesSyncUtils.bookmarks.wipe();
- // clear keys so when each test finds a different server it accepts its keys.
- Service.collectionKeys.clear();
-}
-
-let bookmarksEngine;
-
-add_task(async function setup() {
- bookmarksEngine = Service.engineManager.get("bookmarks");
-
- Service.recordTelemetryEvent = (object, method, value, extra = undefined) => {
- recordedEvents.push({ object, method, value, extra });
- };
-});
-
-add_task(async function test_responder_error() {
- let server = await makeServer();
-
- // sync so the collection is created.
- await Service.sync();
-
- let request = {
- request: "upload",
- ids: [Utils.makeGUID()],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- // mock the responder to simulate an error.
- responder._fetchItemsToUpload = async function() {
- throw new Error("oh no!");
- };
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "failed",
- value: undefined,
- extra: { flowID: request.flowID,
- numIDs: "0",
- failureReason: '{"name":"unexpectederror","error":"Error: oh no!"}',
- }
- },
- ]);
-
- await cleanup(server);
-});
-
-add_task(async function test_responder_no_items() {
- let server = await makeServer();
-
- // sync so the collection is created.
- await Service.sync();
-
- let request = {
- request: "upload",
- ids: [Utils.makeGUID()],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "finished",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "0"},
- },
- ]);
-
- await cleanup(server);
-});
-
-// One item requested and we have it locally, but it's not yet on the server.
-add_task(async function test_responder_upload() {
- let server = await makeServer();
-
- // Pretend we've already synced this bookmark, so that we can ensure it's
- // uploaded in response to our repair request.
- let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
- title: "Get Firefox",
- url: "http://getfirefox.com/",
- source: PlacesUtils.bookmarks.SOURCES.SYNC });
-
- await Service.sync();
- deepEqual(getServerBookmarks(server).keys().sort(), [
- "menu",
- "mobile",
- "toolbar",
- "unfiled",
- ], "Should only upload roots on first sync");
-
- let request = {
- request: "upload",
- ids: [bm.guid],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "uploading",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "1"},
- },
- ]);
-
- await Service.sync();
- deepEqual(getServerBookmarks(server).keys().sort(), [
- "menu",
- "mobile",
- "toolbar",
- "unfiled",
- bm.guid,
- ].sort(), "Should upload requested bookmark on second sync");
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "finished",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "1"},
- },
- ]);
-
- await cleanup(server);
-});
-
-// One item requested and we have it locally and it's already on the server.
-// As it was explicitly requested, we should upload it.
-add_task(async function test_responder_item_exists_locally() {
- let server = await makeServer();
-
- let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
- title: "Get Firefox",
- url: "http://getfirefox.com/" });
- // first sync to get the item on the server.
- _("Syncing to get item on the server");
- await Service.sync();
-
- // issue a repair request for it.
- let request = {
- request: "upload",
- ids: [bm.guid],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- await responder.repair(request, null);
-
- // We still re-upload the item.
- checkRecordedEvents([
- { object: "repairResponse",
- method: "uploading",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "1"},
- },
- ]);
-
- _("Syncing to do the upload.");
- await Service.sync();
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "finished",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "1"},
- },
- ]);
- await cleanup(server);
-});
-
-add_task(async function test_responder_tombstone() {
- let server = await makeServer();
-
- // TODO: Request an item for which we have a tombstone locally. Decide if
- // we want to store tombstones permanently for this. In the integration
- // test, we can also try requesting a deleted child or ancestor.
-
- // For now, we'll handle this identically to `test_responder_missing_items`.
- // Bug 1343103 is a follow-up to better handle this.
- await cleanup(server);
-});
-
-add_task(async function test_responder_missing_items() {
- let server = await makeServer();
-
- let fxBmk = await PlacesUtils.bookmarks.insert({
- parentGuid: PlacesUtils.bookmarks.unfiledGuid,
- title: "Get Firefox",
- url: "http://getfirefox.com/",
- });
- let tbBmk = await PlacesUtils.bookmarks.insert({
- parentGuid: PlacesUtils.bookmarks.unfiledGuid,
- title: "Get Thunderbird",
- url: "http://getthunderbird.com/",
- // Pretend we've already synced Thunderbird.
- source: PlacesUtils.bookmarks.SOURCES.SYNC,
- });
-
- await Service.sync();
- deepEqual(getServerBookmarks(server).keys().sort(), [
- "menu",
- "mobile",
- "toolbar",
- "unfiled",
- fxBmk.guid,
- ].sort(), "Should upload roots and Firefox on first sync");
-
- _("Request Firefox, Thunderbird, and nonexistent GUID");
- let request = {
- request: "upload",
- ids: [fxBmk.guid, tbBmk.guid, Utils.makeGUID()],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "uploading",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "2"},
- },
- ]);
-
- _("Sync after requesting IDs");
- await Service.sync();
- deepEqual(getServerBookmarks(server).keys().sort(), [
- "menu",
- "mobile",
- "toolbar",
- "unfiled",
- fxBmk.guid,
- tbBmk.guid,
- ].sort(), "Second sync should upload Thunderbird; skip nonexistent");
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "finished",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "2"},
- },
- ]);
-
- await cleanup(server);
-});
-
-add_task(async function test_non_syncable() {
- let server = await makeServer();
-
- await Service.sync(); // to create the collections on the server.
-
- // Creates the left pane queries as a side effect.
- let leftPaneId = PlacesUIUtils.leftPaneFolderId;
- _(`Left pane root ID: ${leftPaneId}`);
- await PlacesTestUtils.promiseAsyncUpdates();
-
- // A child folder of the left pane root, containing queries for the menu,
- // toolbar, and unfiled queries.
- let allBookmarksId = PlacesUIUtils.leftPaneQueries.AllBookmarks;
- let allBookmarksGuid = await PlacesUtils.promiseItemGuid(allBookmarksId);
-
- let unfiledQueryId = PlacesUIUtils.leftPaneQueries.UnfiledBookmarks;
- let unfiledQueryGuid = await PlacesUtils.promiseItemGuid(unfiledQueryId);
-
- // Put the "Bookmarks Menu" on the server to simulate old bugs.
- let bookmarksMenuQueryId = PlacesUIUtils.leftPaneQueries.BookmarksMenu;
- let bookmarksMenuQueryGuid = await PlacesUtils.promiseItemGuid(bookmarksMenuQueryId);
- let collection = getServerBookmarks(server);
- collection.insert(bookmarksMenuQueryGuid, "doesn't matter");
-
- // Explicitly request the unfiled and allBookmarksGuid queries; these will
- // get tombstones. Because the BookmarksMenu is already on the server it
- // should be removed even though it wasn't requested. We should ignore the
- // toolbar query as it wasn't explicitly requested and isn't on the server.
- let request = {
- request: "upload",
- ids: [allBookmarksGuid, unfiledQueryGuid],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "uploading",
- value: undefined,
- // Tombstones for the 2 items we requested and for bookmarksMenu
- extra: {flowID: request.flowID, numIDs: "3"},
- },
- ]);
-
- _("Sync to upload tombstones for items");
- await Service.sync();
-
- let toolbarQueryId = PlacesUIUtils.leftPaneQueries.BookmarksToolbar;
- let menuQueryId = PlacesUIUtils.leftPaneQueries.BookmarksMenu;
- let queryGuids = [
- allBookmarksGuid,
- await PlacesUtils.promiseItemGuid(toolbarQueryId),
- await PlacesUtils.promiseItemGuid(menuQueryId),
- unfiledQueryGuid,
- ];
-
- deepEqual(collection.keys().sort(), [
- // We always upload roots on the first sync.
- "menu",
- "mobile",
- "toolbar",
- "unfiled",
- ...request.ids,
- bookmarksMenuQueryGuid,
- ].sort(), "Should upload roots and queries on first sync");
-
- for (let guid of queryGuids) {
- let wbo = collection.wbo(guid);
- if (request.ids.indexOf(guid) >= 0 || guid == bookmarksMenuQueryGuid) {
- // explicitly requested or already on the server, so should have a tombstone.
- let payload = JSON.parse(JSON.parse(wbo.payload).ciphertext);
- ok(payload.deleted, `Should upload tombstone for left pane query ${guid}`);
- } else {
- // not explicitly requested and not on the server at the start, so should
- // not be on the server now.
- ok(!wbo, `Should not upload anything for left pane query ${guid}`);
- }
- }
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "finished",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "3"},
- },
- ]);
-
- await cleanup(server);
-});
-
-add_task(async function test_folder_descendants() {
- let server = await makeServer();
-
- let parentFolder = await PlacesUtils.bookmarks.insert({
- type: PlacesUtils.bookmarks.TYPE_FOLDER,
- parentGuid: PlacesUtils.bookmarks.menuGuid,
- title: "Parent folder",
- });
- let childFolder = await PlacesUtils.bookmarks.insert({
- type: PlacesUtils.bookmarks.TYPE_FOLDER,
- parentGuid: parentFolder.guid,
- title: "Child folder",
- });
- // This item is in parentFolder and *should not* be uploaded as part of
- // the repair even though we explicitly request its parent.
- let existingChildBmk = await PlacesUtils.bookmarks.insert({
- parentGuid: parentFolder.guid,
- title: "Get Firefox",
- url: "http://firefox.com",
- });
- // This item is in parentFolder and *should* be uploaded as part of
- // the repair because we explicitly request its ID.
- let childSiblingBmk = await PlacesUtils.bookmarks.insert({
- parentGuid: parentFolder.guid,
- title: "Get Thunderbird",
- url: "http://getthunderbird.com",
- });
-
- _("Initial sync to upload roots and parent folder");
- await Service.sync();
-
- let initialRecordIds = [
- "menu",
- "mobile",
- "toolbar",
- "unfiled",
- parentFolder.guid,
- existingChildBmk.guid,
- childFolder.guid,
- childSiblingBmk.guid,
- ].sort();
- deepEqual(getServerBookmarks(server).keys().sort(), initialRecordIds,
- "Should upload roots and partial folder contents on first sync");
-
- _("Insert missing bookmarks locally to request later");
- // Note that the fact we insert the bookmarks via PlacesSyncUtils.bookmarks.insert
- // means that we are pretending Sync itself wrote them, hence they aren't
- // considered "changed" locally so never get uploaded.
- let childBmk = await PlacesSyncUtils.bookmarks.insert({
- kind: "bookmark",
- recordId: Utils.makeGUID(),
- parentRecordId: parentFolder.guid,
- title: "Get Firefox",
- url: "http://getfirefox.com",
- });
- let grandChildBmk = await PlacesSyncUtils.bookmarks.insert({
- kind: "bookmark",
- recordId: Utils.makeGUID(),
- parentRecordId: childFolder.guid,
- title: "Bugzilla",
- url: "https://bugzilla.mozilla.org",
- });
- let grandChildSiblingBmk = await PlacesSyncUtils.bookmarks.insert({
- kind: "bookmark",
- recordId: Utils.makeGUID(),
- parentRecordId: childFolder.guid,
- title: "Mozilla",
- url: "https://mozilla.org",
- });
-
- _("Sync again; server contents shouldn't change");
- await Service.sync();
- deepEqual(getServerBookmarks(server).keys().sort(), initialRecordIds,
- "Second sync should not upload missing bookmarks");
-
- // This assumes the parent record on the server is correct, and the server
- // is just missing the children. This isn't a correct assumption if the
- // parent's `children` array is wrong, or if the parent and children disagree.
- _("Request missing bookmarks");
- let request = {
- request: "upload",
- ids: [
- // Already on server (but still uploaded as they are explicitly requested)
- parentFolder.guid,
- childSiblingBmk.guid,
- // Explicitly upload these. We should also upload `grandChildBmk`,
- // since it's a descendant of `parentFolder` and we requested its
- // ancestor.
- childBmk.recordId,
- grandChildSiblingBmk.recordId],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "uploading",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "5"},
- },
- ]);
-
- _("Sync after requesting repair; should upload missing records");
- await Service.sync();
- deepEqual(getServerBookmarks(server).keys().sort(), [
- ...initialRecordIds,
- childBmk.recordId,
- grandChildBmk.recordId,
- grandChildSiblingBmk.recordId,
- ].sort(), "Third sync should upload requested items");
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "finished",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "5"},
- },
- ]);
-
- await cleanup(server);
-});
-
-// Error handling.
-add_task(async function test_aborts_unknown_request() {
- let server = await makeServer();
-
- let request = {
- request: "not-upload",
- ids: [],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "aborted",
- value: undefined,
- extra: { flowID: request.flowID,
- reason: "Don't understand request type 'not-upload'",
- },
- },
- ]);
- await cleanup(server);
-});
-
-add_task(async function test_upload_fail() {
- let server = await makeServer();
-
- // Pretend we've already synced this bookmark, so that we can ensure it's
- // uploaded in response to our repair request.
- let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
- title: "Get Firefox",
- url: "http://getfirefox.com/",
- source: PlacesUtils.bookmarks.SOURCES.SYNC });
-
- await Service.sync();
- let request = {
- request: "upload",
- ids: [bm.guid],
- flowID: Utils.makeGUID(),
- };
- let responder = new BookmarkRepairResponder();
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "uploading",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "1"},
- },
- ]);
-
- // This sync would normally upload the item - arrange for it to fail.
- let engine = Service.engineManager.get("bookmarks");
- let oldCreateRecord = engine._createRecord;
- engine._createRecord = async function(id) {
- return "anything"; // doesn't have an "encrypt"
- };
-
- let numFailures = 0;
- let numSuccesses = 0;
- function onUploaded(subject, data) {
- if (data != "bookmarks") {
- return;
- }
- if (subject.failed) {
- numFailures += 1;
- } else {
- numSuccesses += 1;
- }
- }
- Svc.Obs.add("weave:engine:sync:uploaded", onUploaded, this);
-
- await Service.sync();
-
- equal(numFailures, 1);
- equal(numSuccesses, 0);
-
- // should be no recorded events
- checkRecordedEvents([]);
-
- // restore the error injection so next sync succeeds - the repair should
- // restart
- engine._createRecord = oldCreateRecord;
- await responder.repair(request, null);
-
- checkRecordedEvents([
- { object: "repairResponse",
- method: "uploading",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "1"},
- },
- ]);
-
- await Service.sync();
- checkRecordedEvents([
- { object: "repairResponse",
- method: "finished",
- value: undefined,
- extra: {flowID: request.flowID, numIDs: "1"},
- },
- ]);
-
- equal(numFailures, 1);
- equal(numSuccesses, 1);
-
- Svc.Obs.remove("weave:engine:sync:uploaded", onUploaded, this);
- await cleanup(server);
-});
-
-add_task(async function teardown() {
- Svc.Prefs.reset("engine.bookmarks.validation.enabled");
-});
deleted file mode 100644
--- a/services/sync/tests/unit/test_bookmark_validator.js
+++ /dev/null
@@ -1,448 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Components.utils.import("resource://services-sync/bookmark_validator.js");
-Components.utils.import("resource://services-sync/util.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].indexOf("A") >= 0);
- ok(c.cycles[0].indexOf("B") >= 0);
-});
-
-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.indexOf("B") >= 0);
- ok(c.multipleParents[0].parents.indexOf("C") >= 0);
-});
-
-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: "B", 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,
- "annos": [{
- "name": "Places/SmartBookmark",
- "flags": 0,
- "expires": 4,
- "value": "RecentTags"
- }],
- "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,
- "annos": [{
- "name": "places/excludeFromBackup",
- "flags": 0,
- "expires": 4,
- "value": 1
- }, {
- "name": "PlacesOrganizer/OrganizerFolder",
- "flags": 0,
- "expires": 4,
- "value": 7
- }],
- "type": "text/x-moz-place-container",
- "children": [{
- "guid": "eeeeeeeeeeee",
- "title": "History",
- "annos": [{
- "name": "places/excludeFromBackup",
- "flags": 0,
- "expires": 4,
- "value": 1
- }, {
- "name": "PlacesOrganizer/OrganizerQuery",
- "flags": 0,
- "expires": 4,
- "value": "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.
- duration,
- version: validator.version,
- recordCount: server.length,
- problems: problemData,
- };
- 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 },
- ]);
-});
--- a/services/sync/tests/unit/test_collections_recovery.js
+++ b/services/sync/tests/unit/test_collections_recovery.js
@@ -4,17 +4,17 @@
// Verify that we wipe the server if we have to regenerate keys.
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
initTestLogging("Trace");
add_task(async function test_missing_crypto_collection() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let johnHelper = track_collections_helper();
let johnU = johnHelper.with_updated_collection;
let johnColls = johnHelper.collections;
let empty = false;
function maybe_empty(handler) {
return function(request, response) {
--- a/services/sync/tests/unit/test_corrupt_keys.js
+++ b/services/sync/tests/unit/test_corrupt_keys.js
@@ -10,17 +10,17 @@ Cu.import("resource://services-sync/engi
Cu.import("resource://services-sync/engines/history.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/status.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
add_task(async function test_locally_changed_keys() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let hmacErrorCount = 0;
function counting(f) {
return async function() {
hmacErrorCount++;
return f.call(this);
};
}
deleted file mode 100644
--- a/services/sync/tests/unit/test_doctor.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const { Doctor, REPAIR_ADVANCE_PERIOD } = Cu.import("resource://services-sync/doctor.js", {});
-Cu.import("resource://gre/modules/Services.jsm");
-
-initTestLogging("Trace");
-
-function mockDoctor(mocks) {
- // Clone the object and put mocks in that.
- return Object.assign({}, Doctor, mocks);
-}
-
-add_task(async function test_validation_interval() {
- let now = 1000;
- let doctor = mockDoctor({
- _now() {
- // note that the function being mocked actually returns seconds.
- return now;
- },
- });
-
- let engine = {
- name: "test-engine",
- getValidator() {
- return {
- validate(e) {
- return {};
- }
- };
- },
- };
-
- // setup prefs which enable test-engine validation.
- Services.prefs.setBoolPref("services.sync.engine.test-engine.validation.enabled", true);
- Services.prefs.setIntPref("services.sync.engine.test-engine.validation.percentageChance", 100);
- Services.prefs.setIntPref("services.sync.engine.test-engine.validation.maxRecords", 1);
- // And say we should validate every 10 seconds.
- Services.prefs.setIntPref("services.sync.engine.test-engine.validation.interval", 10);
-
- deepEqual(doctor._getEnginesToValidate([engine]), {
- "test-engine": {
- engine,
- maxRecords: 1,
- }
- });
- // We haven't advanced the timestamp, so we should not validate again.
- deepEqual(doctor._getEnginesToValidate([engine]), {});
- // Advance our clock by 11 seconds.
- now += 11;
- // We should validate again.
- deepEqual(doctor._getEnginesToValidate([engine]), {
- "test-engine": {
- engine,
- maxRecords: 1,
- }
- });
-});
-
-add_task(async function test_repairs_start() {
- let repairStarted = false;
- let problems = {
- missingChildren: ["a", "b", "c"],
- };
- let validator = {
- validate(engine) {
- return problems;
- },
- canValidate() {
- return Promise.resolve(true);
- }
- };
- let engine = {
- name: "test-engine",
- getValidator() {
- return validator;
- }
- };
- let requestor = {
- async startRepairs(validationInfo, flowID) {
- ok(flowID, "got a flow ID");
- equal(validationInfo, problems);
- repairStarted = true;
- return true;
- },
- tryServerOnlyRepairs() {
- return false;
- }
- };
- let doctor = mockDoctor({
- _getEnginesToValidate(recentlySyncedEngines) {
- deepEqual(recentlySyncedEngines, [engine]);
- return {
- "test-engine": { engine, maxRecords: -1 }
- };
- },
- _getRepairRequestor(engineName) {
- equal(engineName, engine.name);
- return requestor;
- },
- _shouldRepair(e) {
- return true;
- },
- });
- let promiseValidationDone = promiseOneObserver("weave:engine:validate:finish");
- await doctor.consult([engine]);
- await promiseValidationDone;
- ok(repairStarted);
-});
-
-add_task(async function test_repairs_advanced_daily() {
- let repairCalls = 0;
- let requestor = {
- async continueRepairs() {
- repairCalls++;
- },
- tryServerOnlyRepairs() {
- return false;
- }
- };
- // start now at just after REPAIR_ADVANCE_PERIOD so we do a a first one.
- let now = REPAIR_ADVANCE_PERIOD + 1;
- let doctor = mockDoctor({
- _getEnginesToValidate() {
- return {}; // no validations.
- },
- _runValidators() {
- // do nothing.
- },
- _getAllRepairRequestors() {
- return {
- foo: requestor,
- };
- },
- _now() {
- return now;
- },
- });
- await doctor.consult();
- equal(repairCalls, 1);
- now += 10; // 10 seconds later...
- await doctor.consult();
- // should not have done another repair yet - it's too soon.
- equal(repairCalls, 1);
- // advance our pretend clock by the advance period (eg, day)
- now += REPAIR_ADVANCE_PERIOD;
- await doctor.consult();
- // should have done another repair
- equal(repairCalls, 2);
-});
-
-add_task(async function test_repairs_skip_if_cant_vaidate() {
- let validator = {
- canValidate() {
- return Promise.resolve(false);
- },
- validate() {
- ok(false, "Shouldn't validate");
- }
- };
- let engine = {
- name: "test-engine",
- getValidator() {
- return validator;
- }
- };
- let requestor = {
- async startRepairs(validationInfo, flowID) {
- ok(false, "Never should start repairs");
- },
- tryServerOnlyRepairs() {
- return false;
- }
- };
- let doctor = mockDoctor({
- _getEnginesToValidate(recentlySyncedEngines) {
- deepEqual(recentlySyncedEngines, [engine]);
- return {
- "test-engine": { engine, maxRecords: -1 }
- };
- },
- _getRepairRequestor(engineName) {
- equal(engineName, engine.name);
- return requestor;
- },
- });
- await doctor.consult([engine]);
-});
--- a/services/sync/tests/unit/test_engine_changes_during_sync.js
+++ b/services/sync/tests/unit/test_engine_changes_during_sync.js
@@ -31,17 +31,17 @@ async function cleanup(engine, server) {
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
await promiseStopServer(server);
}
add_task(async function test_history_change_during_sync() {
_("Ensure that we don't bump the score when applying history records.");
- enableValidationPrefs();
+ forceBookmarkValidation();
let engine = Service.engineManager.get("history");
let server = await serverForEnginesWithKeys({"foo": "password"}, [engine]);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("history");
// Override `uploadOutgoing` to insert a record while we're applying
// changes. The tracker should ignore this change.
@@ -85,17 +85,17 @@ add_task(async function test_history_cha
engine._uploadOutgoing = uploadOutgoing;
await cleanup(engine, server);
}
});
add_task(async function test_passwords_change_during_sync() {
_("Ensure that we don't bump the score when applying passwords.");
- enableValidationPrefs();
+ forceBookmarkValidation();
let engine = Service.engineManager.get("passwords");
let server = await serverForEnginesWithKeys({"foo": "password"}, [engine]);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
let uploadOutgoing = engine._uploadOutgoing;
engine._uploadOutgoing = async function() {
@@ -142,17 +142,17 @@ add_task(async function test_passwords_c
}
});
add_task(async function test_prefs_change_during_sync() {
_("Ensure that we don't bump the score when applying prefs.");
const TEST_PREF = "services.sync.prefs.sync.test.duringSync";
- enableValidationPrefs();
+ forceBookmarkValidation();
let engine = Service.engineManager.get("prefs");
let server = await serverForEnginesWithKeys({"foo": "password"}, [engine]);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("prefs");
let uploadOutgoing = engine._uploadOutgoing;
engine._uploadOutgoing = async function() {
@@ -201,17 +201,17 @@ add_task(async function test_prefs_chang
await cleanup(engine, server);
Services.prefs.clearUserPref(TEST_PREF);
}
});
add_task(async function test_forms_change_during_sync() {
_("Ensure that we don't bump the score when applying form records.");
- enableValidationPrefs();
+ forceBookmarkValidation();
let engine = Service.engineManager.get("forms");
let server = await serverForEnginesWithKeys({"foo": "password"}, [engine]);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("forms");
let uploadOutgoing = engine._uploadOutgoing;
engine._uploadOutgoing = async function() {
@@ -259,17 +259,17 @@ add_task(async function test_forms_chang
engine._uploadOutgoing = uploadOutgoing;
await cleanup(engine, server);
}
});
add_task(async function test_bookmark_change_during_sync() {
_("Ensure that we track bookmark changes made during a sync.");
- enableValidationPrefs();
+ forceBookmarkValidation();
// Already-tracked bookmarks that shouldn't be uploaded during the first sync.
let bzBmk = await PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.menuGuid,
url: "https://bugzilla.mozilla.org/",
title: "Bugzilla",
});
_(`Bugzilla GUID: ${bzBmk.guid}`);
--- a/services/sync/tests/unit/test_errorhandler_1.js
+++ b/services/sync/tests/unit/test_errorhandler_1.js
@@ -59,17 +59,17 @@ async function clean() {
await Service.startOver();
await promiseLogReset;
Status.resetSync();
Status.resetBackoff();
errorHandler.didReportProlongedError = false;
}
add_task(async function test_401_logout() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
await sync_and_validate_telem();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
@@ -104,17 +104,17 @@ add_task(async function test_401_logout(
do_check_false(Service.isLoggedIn);
// Clean up.
await Service.startOver();
await promiseStopServer(server);
});
add_task(async function test_credentials_changed_logout() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
await sync_and_validate_telem();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
do_check_true(Service.isLoggedIn);
@@ -359,17 +359,17 @@ add_task(function test_shouldReportLogin
do_check_true(errorHandler.shouldReportError());
// But any other status with a missing clusterURL is treated as a mid-sync
// 401 (ie, should be treated as a node reassignment)
Status.login = LOGIN_SUCCEEDED;
do_check_false(errorHandler.shouldReportError());
});
add_task(async function test_login_syncAndReportErrors_non_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test non-network errors are reported
// when calling syncAndReportErrors
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
Service.identity.resetSyncKeyBundle();
let promiseObserved = promiseOneObserver("weave:ui:login:error");
@@ -379,17 +379,17 @@ add_task(async function test_login_syncA
await promiseObserved;
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
await clean();
await promiseStopServer(server);
});
add_task(async function test_sync_syncAndReportErrors_non_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test non-network errors are reported
// when calling syncAndReportErrors
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
await Service.sync();
@@ -412,17 +412,17 @@ add_task(async function test_sync_syncAn
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
// If we clean this tick, telemetry won't get the right error
await Async.promiseYield();
await clean();
await promiseStopServer(server);
});
add_task(async function test_login_syncAndReportErrors_prolonged_non_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged, non-network errors are
// reported when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
Service.identity.resetSyncKeyBundle();
let promiseObserved = promiseOneObserver("weave:ui:login:error");
@@ -432,17 +432,17 @@ add_task(async function test_login_syncA
await promiseObserved;
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
await clean();
await promiseStopServer(server);
});
add_task(async function test_sync_syncAndReportErrors_prolonged_non_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged, non-network errors are
// reported when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
await Service.sync();
@@ -465,17 +465,17 @@ add_task(async function test_sync_syncAn
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
// If we clean this tick, telemetry won't get the right error
await Async.promiseYield();
await clean();
await promiseStopServer(server);
});
add_task(async function test_login_syncAndReportErrors_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test network errors are reported when calling syncAndReportErrors.
await configureIdentity({username: "broken.wipe"});
Service.clusterURL = fakeServerUrl;
let promiseObserved = promiseOneObserver("weave:ui:login:error");
setLastSync(NON_PROLONGED_ERROR_DURATION);
@@ -484,34 +484,34 @@ add_task(async function test_login_syncA
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
await clean();
});
add_task(async function test_sync_syncAndReportErrors_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test network errors are reported when calling syncAndReportErrors.
Services.io.offline = true;
let promiseUISyncError = promiseOneObserver("weave:ui:sync:error");
setLastSync(NON_PROLONGED_ERROR_DURATION);
errorHandler.syncAndReportErrors();
await promiseUISyncError;
do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
Services.io.offline = false;
await clean();
});
add_task(async function test_login_syncAndReportErrors_prolonged_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged, network errors are reported
// when calling syncAndReportErrors.
await configureIdentity({username: "johndoe"});
Service.clusterURL = fakeServerUrl;
let promiseObserved = promiseOneObserver("weave:ui:login:error");
@@ -520,17 +520,17 @@ add_task(async function test_login_syncA
errorHandler.syncAndReportErrors();
await promiseObserved;
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
await clean();
});
add_task(async function test_sync_syncAndReportErrors_prolonged_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged, network errors are reported
// when calling syncAndReportErrors.
Services.io.offline = true;
let promiseUISyncError = promiseOneObserver("weave:ui:sync:error");
setLastSync(PROLONGED_ERROR_DURATION);
@@ -538,17 +538,17 @@ add_task(async function test_sync_syncAn
await promiseUISyncError;
do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
Services.io.offline = false;
await clean();
});
add_task(async function test_login_prolonged_non_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged, non-network errors are reported
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
Service.identity.resetSyncKeyBundle();
let promiseObserved = promiseOneObserver("weave:ui:login:error");
@@ -558,17 +558,17 @@ add_task(async function test_login_prolo
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_sync_prolonged_non_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged, non-network errors are reported
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
await Service.sync();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
@@ -589,17 +589,17 @@ add_task(async function test_sync_prolon
await promiseObserved;
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_login_prolonged_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged, network errors are reported
await configureIdentity({username: "johndoe"});
Service.clusterURL = fakeServerUrl;
let promiseObserved = promiseOneObserver("weave:ui:login:error");
setLastSync(PROLONGED_ERROR_DURATION);
@@ -607,17 +607,17 @@ add_task(async function test_login_prolo
await promiseObserved;
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await clean();
});
add_task(async function test_sync_prolonged_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged, network errors are reported
Services.io.offline = true;
let promiseUISyncError = promiseOneObserver("weave:ui:sync:error");
setLastSync(PROLONGED_ERROR_DURATION);
await Service.sync();
@@ -625,17 +625,17 @@ add_task(async function test_sync_prolon
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
Services.io.offline = false;
await clean();
});
add_task(async function test_login_non_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test non-network errors are reported
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
Service.identity.resetSyncKeyBundle();
let promiseObserved = promiseOneObserver("weave:ui:login:error");
@@ -645,17 +645,17 @@ add_task(async function test_login_non_n
do_check_eq(Status.login, LOGIN_FAILED_NO_PASSPHRASE);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_sync_non_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test non-network errors are reported
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
// By calling sync, we ensure we're logged in.
await Service.sync();
do_check_eq(Status.sync, SYNC_SUCCEEDED);
@@ -671,17 +671,17 @@ add_task(async function test_sync_non_ne
do_check_eq(Status.sync, CREDENTIALS_CHANGED);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_login_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await configureIdentity({username: "johndoe"});
Service.clusterURL = fakeServerUrl;
let promiseObserved = promiseOneObserver("weave:ui:clear-error");
// Test network errors are not reported.
setLastSync(NON_PROLONGED_ERROR_DURATION);
@@ -690,17 +690,17 @@ add_task(async function test_login_netwo
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
do_check_false(errorHandler.didReportProlongedError);
Services.io.offline = false;
await clean();
});
add_task(async function test_sync_network_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test network errors are not reported.
Services.io.offline = true;
let promiseSyncFinished = promiseOneObserver("weave:ui:sync:finish");
setLastSync(NON_PROLONGED_ERROR_DURATION);
await Service.sync();
@@ -708,17 +708,17 @@ add_task(async function test_sync_networ
do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
do_check_false(errorHandler.didReportProlongedError);
Services.io.offline = false;
await clean();
});
add_task(async function test_sync_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test server maintenance errors are not reported.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
const BACKOFF = 42;
engine.enabled = true;
engine.exception = {status: 503,
@@ -743,17 +743,17 @@ add_task(async function test_sync_server
do_check_eq(Status.sync, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_info_collections_login_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test info/collections server maintenance errors are not reported.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.info"}, server);
let backoffInterval;
@@ -783,17 +783,17 @@ add_task(async function test_info_collec
do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
await clean();
await promiseStopServer(server);
});
add_task(async function test_meta_global_login_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test meta/global server maintenance errors are not reported.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.meta"}, server);
let backoffInterval;
--- a/services/sync/tests/unit/test_errorhandler_2.js
+++ b/services/sync/tests/unit/test_errorhandler_2.js
@@ -94,17 +94,17 @@ async function clean() {
await promiseLogReset;
Status.resetSync();
Status.resetBackoff();
errorHandler.didReportProlongedError = false;
removeLogFiles();
}
add_task(async function test_crypto_keys_login_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
Status.resetSync();
// Test crypto/keys server maintenance errors are not reported.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.keys"}, server);
@@ -135,17 +135,17 @@ add_task(async function test_crypto_keys
do_check_false(errorHandler.didReportProlongedError);
Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
await clean();
await promiseStopServer(server);
});
add_task(async function test_sync_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged server maintenance errors are reported.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
const BACKOFF = 42;
engine.enabled = true;
engine.exception = {status: 503,
@@ -166,17 +166,17 @@ add_task(async function test_sync_prolon
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await promiseStopServer(server);
await clean();
});
add_task(async function test_info_collections_login_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test info/collections prolonged server maintenance errors are reported.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.info"}, server);
let backoffInterval;
@@ -197,17 +197,17 @@ add_task(async function test_info_collec
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_meta_global_login_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test meta/global prolonged server maintenance errors are reported.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.meta"}, server);
let backoffInterval;
@@ -228,17 +228,17 @@ add_task(async function test_meta_global
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_download_crypto_keys_login_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test crypto/keys prolonged server maintenance errors are reported.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.keys"}, server);
// Force re-download of keys
Service.collectionKeys.clear();
@@ -260,17 +260,17 @@ add_task(async function test_download_cr
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_upload_crypto_keys_login_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test crypto/keys prolonged server maintenance errors are reported.
let server = await EHTestsCommon.sync_httpd_setup();
// Start off with an empty account, do not upload a key.
await configureIdentity({username: "broken.keys"}, server);
let backoffInterval;
@@ -291,17 +291,17 @@ add_task(async function test_upload_cryp
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_wipeServer_login_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test that we report prolonged server maintenance errors that occur whilst
// wiping the server.
let server = await EHTestsCommon.sync_httpd_setup();
// Start off with an empty account, do not upload a key.
await configureIdentity({username: "broken.wipe"}, server);
@@ -323,17 +323,17 @@ add_task(async function test_wipeServer_
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_true(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_wipeRemote_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test that we report prolonged server maintenance errors that occur whilst
// wiping all remote devices.
let server = await EHTestsCommon.sync_httpd_setup();
server.registerPathHandler("/1.1/broken.wipe/storage/catapult", EHTestsCommon.service_unavailable);
await configureIdentity({username: "broken.wipe"}, server);
await EHTestsCommon.generateAndUploadKeys();
@@ -364,17 +364,17 @@ add_task(async function test_wipeRemote_
do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
do_check_true(errorHandler.didReportProlongedError);
await promiseStopServer(server);
await clean();
});
add_task(async function test_sync_syncAndReportErrors_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
const BACKOFF = 42;
engine.enabled = true;
@@ -390,17 +390,17 @@ add_task(async function test_sync_syncAn
do_check_eq(Status.sync, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_info_collections_login_syncAndReportErrors_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test info/collections server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.info"}, server);
@@ -422,17 +422,17 @@ add_task(async function test_info_collec
do_check_eq(Status.login, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_meta_global_login_syncAndReportErrors_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test meta/global server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.meta"}, server);
@@ -454,17 +454,17 @@ add_task(async function test_meta_global
do_check_eq(Status.login, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_download_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.keys"}, server);
// Force re-download of keys
@@ -488,17 +488,17 @@ add_task(async function test_download_cr
do_check_eq(Status.login, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_upload_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
// Start off with an empty account, do not upload a key.
await configureIdentity({username: "broken.keys"}, server);
@@ -520,17 +520,17 @@ add_task(async function test_upload_cryp
do_check_eq(Status.login, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_wipeServer_login_syncAndReportErrors_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
// Start off with an empty account, do not upload a key.
await configureIdentity({username: "broken.wipe"}, server);
@@ -552,17 +552,17 @@ add_task(async function test_wipeServer_
do_check_eq(Status.login, SERVER_MAINTENANCE);
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_wipeRemote_syncAndReportErrors_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test that we report prolonged server maintenance errors that occur whilst
// wiping all remote devices.
let server = await EHTestsCommon.sync_httpd_setup();
await configureIdentity({username: "broken.wipe"}, server);
await EHTestsCommon.generateAndUploadKeys();
@@ -589,17 +589,17 @@ add_task(async function test_wipeRemote_
do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_sync_syncAndReportErrors_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test prolonged server maintenance errors are
// reported when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
const BACKOFF = 42;
engine.enabled = true;
@@ -617,17 +617,17 @@ add_task(async function test_sync_syncAn
// didReportProlongedError not touched.
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_info_collections_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test info/collections server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.info"}, server);
@@ -651,17 +651,17 @@ add_task(async function test_info_collec
// didReportProlongedError not touched.
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_meta_global_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test meta/global server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.meta"}, server);
@@ -685,17 +685,17 @@ add_task(async function test_meta_global
// didReportProlongedError not touched.
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_download_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
await EHTestsCommon.setUp(server);
await configureIdentity({username: "broken.keys"}, server);
// Force re-download of keys
@@ -721,17 +721,17 @@ add_task(async function test_download_cr
// didReportProlongedError not touched.
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_upload_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
// Start off with an empty account, do not upload a key.
await configureIdentity({username: "broken.keys"}, server);
@@ -755,17 +755,17 @@ add_task(async function test_upload_cryp
// didReportProlongedError not touched.
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_wipeServer_login_syncAndReportErrors_prolonged_server_maintenance_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test crypto/keys server maintenance errors are reported
// when calling syncAndReportErrors.
let server = await EHTestsCommon.sync_httpd_setup();
// Start off with an empty account, do not upload a key.
await configureIdentity({username: "broken.wipe"}, server);
@@ -789,17 +789,17 @@ add_task(async function test_wipeServer_
// didReportProlongedError not touched.
do_check_false(errorHandler.didReportProlongedError);
await clean();
await promiseStopServer(server);
});
add_task(async function test_sync_engine_generic_fail() {
- enableValidationPrefs();
+ forceBookmarkValidation();
equal(getLogFiles().length, 0);
let server = await EHTestsCommon.sync_httpd_setup();
engine.enabled = true;
engine.sync = async function sync() {
Svc.Obs.notify("weave:engine:sync:error", ENGINE_UNKNOWN_FAIL, "catapult");
};
@@ -842,17 +842,17 @@ add_task(async function test_sync_engine
let syncErrors = sumHistogram("WEAVE_ENGINE_SYNC_ERRORS", { key: "catapult" });
do_check_true(syncErrors, 1);
await clean();
await promiseStopServer(server);
});
add_task(async function test_logs_on_sync_error_despite_shouldReportError() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Ensure that an error is still logged when weave:service:sync:error " +
"is notified, despite shouldReportError returning false.");
let log = Log.repository.getLogger("Sync.ErrorHandler");
Svc.Prefs.set("log.appender.file.logOnError", true);
log.info("TESTING");
@@ -868,17 +868,17 @@ add_task(async function test_logs_on_syn
let logFiles = getLogFiles();
equal(logFiles.length, 1);
do_check_true(logFiles[0].leafName.startsWith("error-sync-"), logFiles[0].leafName);
await clean();
});
add_task(async function test_logs_on_login_error_despite_shouldReportError() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Ensure that an error is still logged when weave:service:login:error " +
"is notified, despite shouldReportError returning false.");
let log = Log.repository.getLogger("Sync.ErrorHandler");
Svc.Prefs.set("log.appender.file.logOnError", true);
log.info("TESTING");
@@ -896,17 +896,17 @@ add_task(async function test_logs_on_log
do_check_true(logFiles[0].leafName.startsWith("error-sync-"), logFiles[0].leafName);
await clean();
});
// This test should be the last one since it monkeypatches the engine object
// and we should only have one engine object throughout the file (bug 629664).
add_task(async function test_engine_applyFailed() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = await EHTestsCommon.sync_httpd_setup();
engine.enabled = true;
delete engine.exception;
engine.sync = async function sync() {
Svc.Obs.notify("weave:engine:sync:applied", {newFailed: 1}, "catapult");
};
--- a/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
+++ b/services/sync/tests/unit/test_errorhandler_sync_checkServerError.js
@@ -66,17 +66,17 @@ async function generateAndUploadKeys(ser
}
add_task(async function run_test() {
validate_all_future_pings();
await engineManager.register(CatapultEngine);
});
add_task(async function test_backoff500() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: HTTP 500 sets backoff status.");
let server = await sync_httpd_setup();
await setUp(server);
let engine = engineManager.get("catapult");
engine.enabled = true;
engine.exception = {status: 500};
@@ -95,17 +95,17 @@ add_task(async function test_backoff500(
} finally {
Status.resetBackoff();
await Service.startOver();
}
await promiseStopServer(server);
});
add_task(async function test_backoff503() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: HTTP 503 with Retry-After header leads to backoff notification and sets backoff status.");
let server = await sync_httpd_setup();
await setUp(server);
const BACKOFF = 42;
let engine = engineManager.get("catapult");
engine.enabled = true;
@@ -133,17 +133,17 @@ add_task(async function test_backoff503(
Status.resetBackoff();
Status.resetSync();
await Service.startOver();
}
await promiseStopServer(server);
});
add_task(async function test_overQuota() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: HTTP 400 with body error code 14 means over quota.");
let server = await sync_httpd_setup();
await setUp(server);
let engine = engineManager.get("catapult");
engine.enabled = true;
engine.exception = {status: 400,
@@ -164,17 +164,17 @@ add_task(async function test_overQuota()
} finally {
Status.resetSync();
await Service.startOver();
}
await promiseStopServer(server);
});
add_task(async function test_service_networkError() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Connection refused error from Service.sync() leads to the right status code.");
let server = await sync_httpd_setup();
await setUp(server);
await promiseStopServer(server);
// Provoke connection refused.
Service.clusterURL = "http://localhost:12345/";
@@ -188,17 +188,17 @@ add_task(async function test_service_net
do_check_eq(Status.service, SYNC_FAILED);
} finally {
Status.resetSync();
await Service.startOver();
}
});
add_task(async function test_service_offline() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Wanting to sync in offline mode leads to the right status code but does not increment the ignorable error count.");
let server = await sync_httpd_setup();
await setUp(server);
await promiseStopServer(server);
Services.io.offline = true;
Services.prefs.setBoolPref("network.dns.offline-localhost", false);
@@ -215,17 +215,17 @@ add_task(async function test_service_off
Status.resetSync();
await Service.startOver();
}
Services.io.offline = false;
Services.prefs.clearUserPref("network.dns.offline-localhost");
});
add_task(async function test_engine_networkError() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Network related exceptions from engine.sync() lead to the right status code.");
let server = await sync_httpd_setup();
await setUp(server);
let engine = engineManager.get("catapult");
engine.enabled = true;
engine.exception = Components.Exception("NS_ERROR_UNKNOWN_HOST",
@@ -244,17 +244,17 @@ add_task(async function test_engine_netw
} finally {
Status.resetSync();
await Service.startOver();
}
await promiseStopServer(server);
});
add_task(async function test_resource_timeout() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = await sync_httpd_setup();
await setUp(server);
let engine = engineManager.get("catapult");
engine.enabled = true;
// Resource throws this when it encounters a timeout.
engine.exception = Components.Exception("Aborting due to channel inactivity.",
deleted file mode 100644
--- a/services/sync/tests/unit/test_form_validator.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Components.utils.import("resource://services-sync/engines/forms.js");
-
-function getDummyServerAndClient() {
- return {
- server: [
- {
- id: "11111",
- guid: "11111",
- name: "foo",
- fieldname: "foo",
- value: "bar",
- },
- {
- id: "22222",
- guid: "22222",
- name: "foo2",
- fieldname: "foo2",
- value: "bar2",
- },
- {
- id: "33333",
- guid: "33333",
- name: "foo3",
- fieldname: "foo3",
- value: "bar3",
- },
- ],
- client: [
- {
- id: "11111",
- guid: "11111",
- name: "foo",
- fieldname: "foo",
- value: "bar",
- },
- {
- id: "22222",
- guid: "22222",
- name: "foo2",
- fieldname: "foo2",
- value: "bar2",
- },
- {
- id: "33333",
- guid: "33333",
- name: "foo3",
- fieldname: "foo3",
- value: "bar3",
- }
- ]
- };
-}
-
-add_task(async function test_valid() {
- let { server, client } = getDummyServerAndClient();
- let validator = new FormValidator();
- let { problemData, clientRecords, records, deletedRecords } =
- await validator.compareClientWithServer(client, server);
- equal(clientRecords.length, 3);
- equal(records.length, 3);
- equal(deletedRecords.length, 0);
- deepEqual(problemData, validator.emptyProblemData());
-});
-
-
-add_task(async function test_formValidatorIgnoresMissingClients() {
- // Since history form records are not deleted from the server, the
- // |FormValidator| shouldn't set the |missingClient| flag in |problemData|.
- let { server, client } = getDummyServerAndClient();
- client.pop();
-
- let validator = new FormValidator();
- let { problemData, clientRecords, records, deletedRecords } =
- await validator.compareClientWithServer(client, server);
-
- equal(clientRecords.length, 2);
- equal(records.length, 3);
- equal(deletedRecords.length, 0);
-
- let expected = validator.emptyProblemData();
- deepEqual(problemData, expected);
-});
--- a/services/sync/tests/unit/test_fxa_node_reassignment.js
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -170,17 +170,17 @@ async function syncAndExpectNodeReassign
}
await deferred.promise;
}
// Check that when we sync we don't request a new token by default - our
// test setup has configured the client with a valid token, and that token
// should be used to form the cluster URL.
add_task(async function test_single_token_fetch() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a normal sync only fetches 1 token");
let numTokenFetches = 0;
function afterTokenFetch() {
numTokenFetches++;
}
@@ -202,17 +202,17 @@ add_task(async function test_single_toke
// that clusterURL we expect.
let expectedClusterURL = server.baseURI + "1.1/johndoe/";
do_check_eq(Service.clusterURL, expectedClusterURL);
await Service.startOver();
await promiseStopServer(server);
});
add_task(async function test_momentary_401_engine() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for engine URLs that's resolved by reassignment.");
let server = await prepareServer();
let john = server.user("johndoe");
_("Enabling the Rotary engine.");
let { engine, tracker } = await registerRotaryEngine();
@@ -258,17 +258,17 @@ add_task(async function test_momentary_4
Service.storageURL + "rotary");
tracker.clearChangedIDs();
Service.engineManager.unregister(engine);
});
// This test ends up being a failing info fetch *after we're already logged in*.
add_task(async function test_momentary_401_info_collections_loggedin() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for info/collections after login that's resolved by reassignment.");
let server = await prepareServer();
_("First sync to prepare server contents.");
await Service.sync();
_("Arrange for info/collections to return a 401.");
@@ -288,17 +288,17 @@ add_task(async function test_momentary_4
"weave:service:sync:finish",
Service.infoURL);
});
// This test ends up being a failing info fetch *before we're logged in*.
// In this case we expect to recover during the login phase - so the first
// sync succeeds.
add_task(async function test_momentary_401_info_collections_loggedout() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for info/collections before login that's resolved by reassignment.");
let oldHandler;
let sawTokenFetch = false;
function afterTokenFetch() {
// After a single token fetch, we undo our evil handleReassign hack, so
@@ -322,17 +322,17 @@ add_task(async function test_momentary_4
do_check_true(sawTokenFetch, "a new token was fetched by this test.");
// and we are done.
await Service.startOver();
await promiseStopServer(server);
});
// This test ends up being a failing meta/global fetch *after we're already logged in*.
add_task(async function test_momentary_401_storage_loggedin() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for any storage URL after login that's resolved by" +
"reassignment.");
let server = await prepareServer();
_("First sync to prepare server contents.");
await Service.sync();
@@ -351,17 +351,17 @@ add_task(async function test_momentary_4
"weave:service:sync:error",
undo,
"weave:service:sync:finish",
Service.storageURL + "meta/global");
});
// This test ends up being a failing meta/global fetch *before we've logged in*.
add_task(async function test_momentary_401_storage_loggedout() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for any storage URL before login, not just engine parts. " +
"Resolved by reassignment.");
let server = await prepareServer();
// Return a 401 for all storage requests.
let oldHandler = server.toplevelHandlers.storage;
server.toplevelHandlers.storage = handleReassign;
--- a/services/sync/tests/unit/test_hmac_error.js
+++ b/services/sync/tests/unit/test_hmac_error.js
@@ -13,17 +13,17 @@ var hmacErrorCount = 0;
let hHE = Service.handleHMACEvent;
Service.handleHMACEvent = async function() {
hmacErrorCount++;
return hHE.call(Service);
};
})();
async function shared_setup() {
- enableValidationPrefs();
+ forceBookmarkValidation();
hmacErrorCount = 0;
// Make sure RotaryEngine is the only one we sync.
let { engine, tracker } = await registerRotaryEngine();
engine.lastSync = 123; // Needs to be non-zero so that tracker is queried.
engine._store.items = {flying: "LNER Class A3 4472",
scotsman: "Flying Scotsman"};
--- a/services/sync/tests/unit/test_interval_triggers.js
+++ b/services/sync/tests/unit/test_interval_triggers.js
@@ -55,17 +55,17 @@ add_task(async function setup() {
// Don't remove stale clients when syncing. This is a test-only workaround
// that lets us add clients directly to the store, without losing them on
// the next sync.
clientsEngine._removeRemoteClient = async (id) => {};
});
add_task(async function test_successful_sync_adjustSyncInterval() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test successful sync calling adjustSyncInterval");
let syncSuccesses = 0;
function onSyncFinish() {
_("Sync success.");
syncSuccesses++;
}
Svc.Obs.add("weave:service:sync:finish", onSyncFinish);
@@ -156,17 +156,17 @@ add_task(async function test_successful_
do_check_eq(scheduler.syncInterval, scheduler.immediateInterval);
Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
await Service.startOver();
await promiseStopServer(server);
});
add_task(async function test_unsuccessful_sync_adjustSyncInterval() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test unsuccessful sync calling adjustSyncInterval");
let syncFailures = 0;
function onSyncError() {
_("Sync error.");
syncFailures++;
}
@@ -264,17 +264,17 @@ add_task(async function test_unsuccessfu
do_check_eq(scheduler.syncInterval, scheduler.immediateInterval);
await Service.startOver();
Svc.Obs.remove("weave:service:sync:error", onSyncError);
await promiseStopServer(server);
});
add_task(async function test_back_triggers_sync() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = sync_httpd_setup();
await setUp(server);
// Single device: no sync triggered.
scheduler.idle = true;
scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime"));
do_check_false(scheduler.idle);
@@ -295,17 +295,17 @@ add_task(async function test_back_trigge
scheduler.setDefaults();
await clientsEngine.resetClient();
await Service.startOver();
await promiseStopServer(server);
});
add_task(async function test_adjust_interval_on_sync_error() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = sync_httpd_setup();
await setUp(server);
let syncFailures = 0;
function onSyncError() {
_("Sync error.");
syncFailures++;
@@ -328,17 +328,17 @@ add_task(async function test_adjust_inte
do_check_eq(scheduler.syncInterval, scheduler.activeInterval);
Svc.Obs.remove("weave:service:sync:error", onSyncError);
await Service.startOver();
await promiseStopServer(server);
});
add_task(async function test_bug671378_scenario() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test scenario similar to bug 671378. This bug appeared when a score
// update occurred that wasn't large enough to trigger a sync so
// scheduleNextSync() was called without a time interval parameter,
// setting nextSync to a non-zero value and preventing the timer from
// being adjusted in the next call to scheduleNextSync().
let server = sync_httpd_setup();
await setUp(server);
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -130,17 +130,17 @@ async function syncAndExpectNodeReassign
Svc.Obs.add(firstNotification, onFirstSync);
await Service.sync();
await deferred.promise;
}
add_task(async function test_momentary_401_engine() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for engine URLs that's resolved by reassignment.");
let server = await prepareServer();
let john = server.user("johndoe");
_("Enabling the Rotary engine.");
let { engine, tracker } = await registerRotaryEngine();
@@ -186,17 +186,17 @@ add_task(async function test_momentary_4
Service.storageURL + "rotary");
tracker.clearChangedIDs();
Service.engineManager.unregister(engine);
});
// This test ends up being a failing fetch *after we're already logged in*.
add_task(async function test_momentary_401_info_collections() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for info/collections that's resolved by reassignment.");
let server = await prepareServer();
_("First sync to prepare server contents.");
await Service.sync();
// Return a 401 for info requests, particularly info/collections.
@@ -211,17 +211,17 @@ add_task(async function test_momentary_4
await syncAndExpectNodeReassignment(server,
"weave:service:sync:error",
undo,
"weave:service:sync:finish",
Service.infoURL);
});
add_task(async function test_momentary_401_storage_loggedin() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for any storage URL, not just engine parts. " +
"Resolved by reassignment.");
let server = await prepareServer();
_("Performing initial sync to ensure we are logged in.");
await Service.sync();
@@ -238,17 +238,17 @@ add_task(async function test_momentary_4
await syncAndExpectNodeReassignment(server,
"weave:service:sync:error",
undo,
"weave:service:sync:finish",
Service.storageURL + "meta/global");
});
add_task(async function test_momentary_401_storage_loggedout() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a failure for any storage URL, not just engine parts. " +
"Resolved by reassignment.");
let server = await prepareServer();
// Return a 401 for all storage requests.
let oldHandler = server.toplevelHandlers.storage;
server.toplevelHandlers.storage = handleReassign;
@@ -262,17 +262,17 @@ add_task(async function test_momentary_4
await syncAndExpectNodeReassignment(server,
"weave:service:login:error",
undo,
"weave:service:sync:finish",
Service.storageURL + "meta/global");
});
add_task(async function test_loop_avoidance_storage() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test that a repeated failure doesn't result in a sync loop " +
"if node reassignment cannot resolve the failure.");
let server = await prepareServer();
// Return a 401 for all storage requests.
let oldHandler = server.toplevelHandlers.storage;
@@ -364,17 +364,17 @@ add_task(async function test_loop_avoida
Svc.Obs.add(firstNotification, onFirstSync);
now = Date.now();
await Service.sync();
await deferred.promise;
});
add_task(async function test_loop_avoidance_engine() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test that a repeated 401 in an engine doesn't result in a sync loop " +
"if node reassignment cannot resolve the failure.");
let server = await prepareServer();
let john = server.user("johndoe");
_("Enabling the Rotary engine.");
let { engine, tracker } = await registerRotaryEngine();
--- a/services/sync/tests/unit/test_password_engine.js
+++ b/services/sync/tests/unit/test_password_engine.js
@@ -25,17 +25,17 @@ async function cleanup(engine, server) {
add_task(async function test_ignored_fields() {
_("Only changes to syncable fields should be tracked");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
- enableValidationPrefs();
+ forceBookmarkValidation();
let login = Services.logins.addLogin(new LoginInfo("https://example.com", "",
null, "username", "password", "", ""));
login.QueryInterface(Ci.nsILoginMetaInfo); // For `guid`.
Svc.Obs.notify("weave:engine:start-tracking");
try {
@@ -62,17 +62,17 @@ add_task(async function test_ignored_fie
add_task(async function test_ignored_sync_credentials() {
_("Sync credentials in login manager should be ignored");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
- enableValidationPrefs();
+ forceBookmarkValidation();
Svc.Obs.notify("weave:engine:start-tracking");
try {
let login = Services.logins.addLogin(new LoginInfo(FXA_PWDMGR_HOST, null,
FXA_PWDMGR_REALM, "fxa-uid", "creds", "", ""));
let noChanges = await engine.pullNewChanges();
@@ -93,17 +93,17 @@ add_task(async function test_password_en
_("Basic password sync test");
let engine = Service.engineManager.get("passwords");
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let collection = server.user("foo").collection("passwords");
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Add new login to upload during first sync");
let newLogin;
{
let login = new LoginInfo("https://example.com", "", null, "username",
"password", "", "");
Services.logins.addLogin(login);
deleted file mode 100644
--- a/services/sync/tests/unit/test_password_validator.js
+++ /dev/null
@@ -1,147 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Components.utils.import("resource://services-sync/engines/passwords.js");
-
-function getDummyServerAndClient() {
- return {
- server: [
- {
- id: "11111",
- guid: "11111",
- hostname: "https://www.11111.com",
- formSubmitURL: "https://www.11111.com/login",
- password: "qwerty123",
- passwordField: "pass",
- username: "foobar",
- usernameField: "user",
- httpRealm: null,
- },
- {
- id: "22222",
- guid: "22222",
- hostname: "https://www.22222.org",
- formSubmitURL: "https://www.22222.org/login",
- password: "hunter2",
- passwordField: "passwd",
- username: "baz12345",
- usernameField: "user",
- httpRealm: null,
- },
- {
- id: "33333",
- guid: "33333",
- hostname: "https://www.33333.com",
- formSubmitURL: "https://www.33333.com/login",
- password: "p4ssw0rd",
- passwordField: "passwad",
- username: "quux",
- usernameField: "user",
- httpRealm: null,
- },
- ],
- client: [
- {
- id: "11111",
- guid: "11111",
- hostname: "https://www.11111.com",
- formSubmitURL: "https://www.11111.com/login",
- password: "qwerty123",
- passwordField: "pass",
- username: "foobar",
- usernameField: "user",
- httpRealm: null,
- },
- {
- id: "22222",
- guid: "22222",
- hostname: "https://www.22222.org",
- formSubmitURL: "https://www.22222.org/login",
- password: "hunter2",
- passwordField: "passwd",
- username: "baz12345",
- usernameField: "user",
- httpRealm: null,
-
- },
- {
- id: "33333",
- guid: "33333",
- hostname: "https://www.33333.com",
- formSubmitURL: "https://www.33333.com/login",
- password: "p4ssw0rd",
- passwordField: "passwad",
- username: "quux",
- usernameField: "user",
- httpRealm: null,
- }
- ]
- };
-}
-
-
-add_task(async function test_valid() {
- let { server, client } = getDummyServerAndClient();
- let validator = new PasswordValidator();
- let { problemData, clientRecords, records, deletedRecords } =
- await validator.compareClientWithServer(client, server);
- equal(clientRecords.length, 3);
- equal(records.length, 3);
- equal(deletedRecords.length, 0);
- deepEqual(problemData, validator.emptyProblemData());
-});
-
-add_task(async function test_missing() {
- let validator = new PasswordValidator();
- {
- let { server, client } = getDummyServerAndClient();
-
- client.pop();
-
- let { problemData, clientRecords, records, deletedRecords } =
- await validator.compareClientWithServer(client, server);
-
- equal(clientRecords.length, 2);
- equal(records.length, 3);
- equal(deletedRecords.length, 0);
-
- let expected = validator.emptyProblemData();
- expected.clientMissing.push("33333");
- deepEqual(problemData, expected);
- }
- {
- let { server, client } = getDummyServerAndClient();
-
- server.pop();
-
- let { problemData, clientRecords, records, deletedRecords } =
- await validator.compareClientWithServer(client, server);
-
- equal(clientRecords.length, 3);
- equal(records.length, 2);
- equal(deletedRecords.length, 0);
-
- let expected = validator.emptyProblemData();
- expected.serverMissing.push("33333");
- deepEqual(problemData, expected);
- }
-});
-
-
-add_task(async function test_deleted() {
- let { server, client } = getDummyServerAndClient();
- let deletionRecord = { id: "444444", guid: "444444", deleted: true };
-
- server.push(deletionRecord);
- let validator = new PasswordValidator();
-
- let { problemData, clientRecords, records, deletedRecords } =
- await validator.compareClientWithServer(client, server);
-
- equal(clientRecords.length, 3);
- equal(records.length, 4);
- deepEqual(deletedRecords, [deletionRecord]);
-
- let expected = validator.emptyProblemData();
- deepEqual(problemData, expected);
-});
--- a/services/sync/tests/unit/test_score_triggers.js
+++ b/services/sync/tests/unit/test_score_triggers.js
@@ -47,17 +47,17 @@ function run_test() {
initTestLogging("Trace");
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
run_next_test();
}
add_task(async function test_tracker_score_updated() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let { engine, tracker } = await registerRotaryEngine();
let scoreUpdated = 0;
function onScoreUpdated() {
scoreUpdated++;
}
@@ -95,17 +95,17 @@ add_task(async function test_sync_trigge
await Service.startOver();
await promiseStopServer(server);
tracker.clearChangedIDs();
Service.engineManager.unregister(engine);
});
add_task(async function test_clients_engine_sync_triggered() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Ensure that client engine score changes trigger a sync.");
// The clients engine is not registered like other engines. Therefore,
// it needs special treatment throughout the code. Here, we verify the
// global score tracker gives it that treatment. See bug 676042 for more.
let server = sync_httpd_setup();
@@ -122,17 +122,17 @@ add_task(async function test_clients_eng
await Service.startOver();
await promiseStopServer(server);
tracker.clearChangedIDs();
Service.engineManager.unregister(engine);
});
add_task(async function test_incorrect_credentials_sync_not_triggered() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Ensure that score changes don't trigger a sync if Status.login != LOGIN_SUCCEEDED.");
let server = sync_httpd_setup();
let { engine, tracker } = await setUp(server);
// Ensure we don't actually try to sync.
function onSyncStart() {
do_throw("Should not get here!");
--- a/services/sync/tests/unit/test_service_detect_upgrade.js
+++ b/services/sync/tests/unit/test_service_detect_upgrade.js
@@ -7,17 +7,17 @@ Cu.import("resource://services-sync/keys
Cu.import("resource://services-sync/engines/tabs.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");
add_task(async function v4_upgrade() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let clients = new ServerCollection();
let meta_global = new ServerWBO("global");
// Tracking info/collections.
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
let collections = collectionsHelper.collections;
@@ -164,17 +164,17 @@ add_task(async function v4_upgrade() {
} finally {
Svc.Prefs.resetBranch("");
await promiseStopServer(server);
}
});
add_task(async function v5_upgrade() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Tracking info/collections.
let collectionsHelper = track_collections_helper();
let upd = collectionsHelper.with_updated_collection;
let keysWBO = new ServerWBO("keys");
let bulkWBO = new ServerWBO("bulk");
let clients = new ServerCollection();
--- a/services/sync/tests/unit/test_service_login.js
+++ b/services/sync/tests/unit/test_service_login.js
@@ -65,17 +65,17 @@ add_task(async function test_not_logged_
do_check_eq(Service._checkSync(), kSyncNotConfigured);
} finally {
Svc.Prefs.resetBranch("");
await promiseStopServer(server);
}
});
add_task(async function test_login_logout() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = setup();
try {
_("Force the initial state.");
Service.status.service = STATUS_OK;
do_check_eq(Service.status.service, STATUS_OK);
@@ -113,17 +113,17 @@ add_task(async function test_login_logou
} finally {
Svc.Prefs.resetBranch("");
await promiseStopServer(server);
}
});
add_task(async function test_login_on_sync() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = setup();
await configureIdentity({ username: "johndoe" }, server);
try {
_("Sync calls login.");
let oldLogin = Service.login;
let loginCalled = false;
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -5,17 +5,17 @@ Cu.import("resource://gre/modules/Log.js
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/keys.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/fakeservices.js");
Cu.import("resource://testing-common/services/sync/utils.js");
add_task(async function run_test() {
- enableValidationPrefs();
+ forceBookmarkValidation();
validate_all_future_pings();
Log.repository.rootLogger.addAppender(new Log.DumpAppender());
let clients = new ServerCollection();
let meta_global = new ServerWBO("global");
let collectionsHelper = track_collections_helper();
--- a/services/sync/tests/unit/test_service_sync_specified.js
+++ b/services/sync/tests/unit/test_service_sync_specified.js
@@ -78,34 +78,34 @@ add_task(async function setup() {
Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
await Service.engineManager.register(SteamEngine);
await Service.engineManager.register(StirlingEngine);
});
add_task(async function test_noEngines() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: An empty array of engines to sync does nothing.");
let server = await setUp();
try {
_("Sync with no engines specified.");
await Service.sync([]);
deepEqual(syncedEngines, [], "no engines were synced");
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_oneEngine() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Only one engine is synced.");
let server = await setUp();
try {
_("Sync with 1 engine specified.");
await Service.sync(["steam"]);
@@ -113,51 +113,51 @@ add_task(async function test_oneEngine()
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_bothEnginesSpecified() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: All engines are synced when specified in the correct order (1).");
let server = await setUp();
try {
_("Sync with both engines specified.");
await Service.sync(["steam", "stirling"]);
deepEqual(syncedEngines, ["steam", "stirling"]);
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_bothEnginesSpecified() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: All engines are synced when specified in the correct order (2).");
let server = await setUp();
try {
_("Sync with both engines specified.");
await Service.sync(["stirling", "steam"]);
deepEqual(syncedEngines, ["stirling", "steam"]);
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_bothEnginesDefault() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: All engines are synced when nothing is specified.");
let server = await setUp();
try {
await Service.sync();
deepEqual(syncedEngines, ["steam", "stirling"]);
--- a/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
+++ b/services/sync/tests/unit/test_service_sync_updateEnabledEngines.js
@@ -84,17 +84,17 @@ add_task(async function setup() {
Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
validate_all_future_pings();
await Service.engineManager.register(SteamEngine);
await Service.engineManager.register(StirlingEngine);
});
add_task(async function test_newAccount() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: New account does not disable locally enabled engines.");
let engine = Service.engineManager.get("steam");
let server = sync_httpd_setup({
"/1.1/johndoe/storage/meta/global": new ServerWBO("global", {}).handler(),
"/1.1/johndoe/storage/steam": new ServerWBO("steam", {}).handler()
});
await setUp(server);
@@ -112,17 +112,17 @@ add_task(async function test_newAccount(
do_check_true(engine.enabled);
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_enabledLocally() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Engine is disabled on remote clients and enabled locally");
Service.syncID = "abcdefghij";
let engine = Service.engineManager.get("steam");
let metaWBO = new ServerWBO("global", {syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
engines: {}});
let server = sync_httpd_setup({
@@ -145,17 +145,17 @@ add_task(async function test_enabledLoca
do_check_true(engine.enabled);
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_disabledLocally() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Engine is enabled on remote clients and disabled locally");
Service.syncID = "abcdefghij";
let engine = Service.engineManager.get("steam");
let metaWBO = new ServerWBO("global", {
syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
engines: {steam: {syncID: engine.syncID,
@@ -189,17 +189,17 @@ add_task(async function test_disabledLoc
do_check_false(engine.enabled);
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_disabledLocally_wipe503() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Engine is enabled on remote clients and disabled locally");
Service.syncID = "abcdefghij";
let engine = Service.engineManager.get("steam");
let metaWBO = new ServerWBO("global", {
syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
engines: {steam: {syncID: engine.syncID,
@@ -232,17 +232,17 @@ add_task(async function test_disabledLoc
await promiseObserved;
do_check_eq(Service.status.sync, SERVER_MAINTENANCE);
await Service.startOver();
await promiseStopServer(server);
});
add_task(async function test_enabledRemotely() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Engine is disabled locally and enabled on a remote client");
Service.syncID = "abcdefghij";
let engine = Service.engineManager.get("steam");
let metaWBO = new ServerWBO("global", {
syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
engines: {steam: {syncID: engine.syncID,
@@ -278,17 +278,17 @@ add_task(async function test_enabledRemo
do_check_eq(metaWBO.data.engines.steam.syncID, engine.syncID);
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_disabledRemotelyTwoClients() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Engine is enabled locally and disabled on a remote client... with two clients.");
Service.syncID = "abcdefghij";
let engine = Service.engineManager.get("steam");
let metaWBO = new ServerWBO("global", {syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
engines: {}});
let server = sync_httpd_setup({
@@ -324,17 +324,17 @@ add_task(async function test_disabledRem
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_disabledRemotely() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Engine is enabled locally and disabled on a remote client");
Service.syncID = "abcdefghij";
let engine = Service.engineManager.get("steam");
let metaWBO = new ServerWBO("global", {syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
engines: {}});
let server = sync_httpd_setup({
@@ -357,17 +357,17 @@ add_task(async function test_disabledRem
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_dependentEnginesEnabledLocally() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Engine is disabled on remote clients and enabled locally");
Service.syncID = "abcdefghij";
let steamEngine = Service.engineManager.get("steam");
let stirlingEngine = Service.engineManager.get("stirling");
let metaWBO = new ServerWBO("global", {syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
engines: {}});
@@ -394,17 +394,17 @@ add_task(async function test_dependentEn
do_check_true(stirlingEngine.enabled);
} finally {
await Service.startOver();
await promiseStopServer(server);
}
});
add_task(async function test_dependentEnginesDisabledLocally() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test: Two dependent engines are enabled on remote clients and disabled locally");
Service.syncID = "abcdefghij";
let steamEngine = Service.engineManager.get("steam");
let stirlingEngine = Service.engineManager.get("stirling");
let metaWBO = new ServerWBO("global", {
syncID: Service.syncID,
storageVersion: STORAGE_VERSION,
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -186,17 +186,17 @@ add_task(async function test_updateClien
do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
do_check_false(scheduler.numClients > 1);
do_check_false(scheduler.idle);
await cleanUpAndGo();
});
add_task(async function test_masterpassword_locked_retry_interval() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test Status.login = MASTER_PASSWORD_LOCKED results in reschedule at MASTER_PASSWORD interval");
let loginFailed = false;
Svc.Obs.add("weave:service:login:error", function onLoginError() {
Svc.Obs.remove("weave:service:login:error", onLoginError);
loginFailed = true;
});
@@ -247,31 +247,31 @@ add_task(async function test_calculateBa
Status.backoffInterval);
do_check_eq(backoffInterval, MAXIMUM_BACKOFF_INTERVAL + 10);
await cleanUpAndGo();
});
add_task(async function test_scheduleNextSync_nowOrPast() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let promiseObserved = promiseOneObserver("weave:service:sync:finish");
let server = sync_httpd_setup();
await setUp(server);
// We're late for a sync...
scheduler.scheduleNextSync(-1);
await promiseObserved;
await cleanUpAndGo(server);
});
add_task(async function test_scheduleNextSync_future_noBackoff() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("scheduleNextSync() uses the current syncInterval if no interval is provided.");
// Test backoffInterval is 0 as expected.
do_check_eq(Status.backoffInterval, 0);
_("Test setting sync interval when nextSync == 0");
scheduler.nextSync = 0;
scheduler.scheduleNextSync();
@@ -312,17 +312,17 @@ add_task(async function test_scheduleNex
scheduler.scheduleNextSync(1);
do_check_true(scheduler.nextSync <= Date.now() + 1);
do_check_eq(scheduler.syncTimer.delay, 1);
await cleanUpAndGo();
});
add_task(async function test_scheduleNextSync_future_backoff() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("scheduleNextSync() will honour backoff in all scheduling requests.");
// Let's take a backoff interval that's bigger than the default sync interval.
const BACKOFF = 7337;
Status.backoffInterval = scheduler.syncInterval + BACKOFF;
_("Test setting sync interval when nextSync == 0");
scheduler.nextSync = 0;
@@ -364,17 +364,17 @@ add_task(async function test_scheduleNex
scheduler.scheduleNextSync(1);
do_check_true(scheduler.nextSync <= Date.now() + Status.backoffInterval);
do_check_eq(scheduler.syncTimer.delay, Status.backoffInterval);
await cleanUpAndGo();
});
add_task(async function test_handleSyncError() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = sync_httpd_setup();
await setUp(server);
// Force sync to fail.
Svc.Prefs.set("firstSync", "notReady");
_("Ensure expected initial environment.");
@@ -429,17 +429,17 @@ add_task(async function test_handleSyncE
let promiseObserved = promiseOneObserver("weave:service:sync:finish");
Svc.Prefs.set("firstSync", "wipeRemote");
scheduler.scheduleNextSync(-1);
await promiseObserved;
await cleanUpAndGo(server);
});
add_task(async function test_client_sync_finish_updateClientMode() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = sync_httpd_setup();
await setUp(server);
// Confirm defaults.
do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
do_check_false(scheduler.idle);
@@ -470,31 +470,31 @@ add_task(async function test_client_sync
do_check_eq(scheduler.syncInterval, scheduler.singleDeviceInterval);
do_check_false(scheduler.numClients > 1);
do_check_false(scheduler.idle);
await cleanUpAndGo(server);
});
add_task(async function test_autoconnect_nextSync_past() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let promiseObserved = promiseOneObserver("weave:service:sync:finish");
// nextSync will be 0 by default, so it's way in the past.
let server = sync_httpd_setup();
await setUp(server);
scheduler.delayedAutoConnect(0);
await promiseObserved;
await cleanUpAndGo(server);
});
add_task(async function test_autoconnect_nextSync_future() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let previousSync = Date.now() + scheduler.syncInterval / 2;
scheduler.nextSync = previousSync;
// nextSync rounds to the nearest second.
let expectedSync = scheduler.nextSync;
let expectedInterval = expectedSync - Date.now() - 1000;
// Ensure we don't actually try to sync (or log in for that matter).
@@ -584,17 +584,17 @@ add_task(async function test_no_autoconn
do_check_eq(Status.service, CLIENT_NOT_CONFIGURED);
do_check_eq(Status.login, LOGIN_FAILED_NO_USERNAME);
await cleanUpAndGo(server);
});
add_task(async function test_autoconnectDelay_pref() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let promiseObserved = promiseOneObserver("weave:service:sync:finish");
Svc.Prefs.set("autoconnectDelay", 1);
let server = sync_httpd_setup();
await setUp(server);
@@ -701,17 +701,17 @@ add_task(async function test_back_deboun
scheduler.observe(null, "idle", Svc.Prefs.get("scheduler.idleTime"));
await promiseNamedTimer(IDLE_OBSERVER_BACK_DELAY * 1.5, {}, "timer");
Svc.Obs.remove("weave:service:login:start", onLoginStart);
await cleanUpAndGo();
});
add_task(async function test_no_sync_node() {
- enableValidationPrefs();
+ forceBookmarkValidation();
// Test when Status.sync == NO_SYNC_NODE_FOUND
// it is not overwritten on sync:finish
let server = sync_httpd_setup();
await setUp(server);
let oldfc = Service._clusterManager._findCluster;
Service._clusterManager._findCluster = () => null;
@@ -723,17 +723,17 @@ add_task(async function test_no_sync_nod
await cleanUpAndGo(server);
} finally {
Service._clusterManager._findCluster = oldfc;
}
});
add_task(async function test_sync_failed_partial_500s() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a 5xx status calls handleSyncError.");
scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF;
let server = sync_httpd_setup();
let engine = Service.engineManager.get("catapult");
engine.enabled = true;
engine.exception = {status: 500};
@@ -783,17 +783,17 @@ add_task(async function test_sync_failed
await resyncDoneObserver;
Svc.Obs.remove("weave:service:sync:start", onSyncStarted);
engine._tracker._store = 0;
await cleanUpAndGo(server);
});
add_task(async function test_sync_failed_partial_400s() {
- enableValidationPrefs();
+ forceBookmarkValidation();
_("Test a non-5xx status doesn't call handleSyncError.");
scheduler._syncErrors = MAX_ERROR_COUNT_BEFORE_BACKOFF;
let server = sync_httpd_setup();
let engine = Service.engineManager.get("catapult");
engine.enabled = true;
engine.exception = {status: 400};
@@ -817,17 +817,17 @@ add_task(async function test_sync_failed
do_check_eq(scheduler._syncErrors, 0);
do_check_true(scheduler.nextSync <= (Date.now() + scheduler.activeInterval));
do_check_true(scheduler.syncTimer.delay <= scheduler.activeInterval);
await cleanUpAndGo(server);
});
add_task(async function test_sync_X_Weave_Backoff() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = sync_httpd_setup();
await setUp(server);
// Use an odd value on purpose so that it doesn't happen to coincide with one
// of the sync intervals.
const BACKOFF = 7337;
@@ -876,17 +876,17 @@ add_task(async function test_sync_X_Weav
// Verify that the next sync is actually going to wait that long.
do_check_true(scheduler.nextSync >= Date.now() + minimumExpectedDelay);
do_check_true(scheduler.syncTimer.delay >= minimumExpectedDelay);
await cleanUpAndGo(server);
});
add_task(async function test_sync_503_Retry_After() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let server = sync_httpd_setup();
await setUp(server);
// Use an odd value on purpose so that it doesn't happen to coincide with one
// of the sync intervals.
const BACKOFF = 7337;
--- a/services/sync/tests/unit/test_telemetry.js
+++ b/services/sync/tests/unit/test_telemetry.js
@@ -12,16 +12,17 @@ Cu.import("resource://services-sync/engi
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/engines/clients.js");
Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://testing-common/services/sync/fxa_utils.js");
Cu.import("resource://testing-common/services/sync/rotaryengine.js");
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://sync-repair/bookmark_validator.jsm");
function SteamStore(engine) {
Store.call(this, "Steam", engine);
}
SteamStore.prototype = {
__proto__: Store.prototype,
@@ -68,17 +69,17 @@ async function cleanAndGo(engine, server
add_task(async function setup() {
initTestLogging("Trace");
// Avoid addon manager complaining about not being initialized
Service.engineManager.unregister("addons");
});
add_task(async function test_basic() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let helper = track_collections_helper();
let upd = helper.with_updated_collection;
let handlers = {
"/1.1/johndoe/info/collections": helper.handler,
"/1.1/johndoe/storage/crypto/keys": upd("crypto", new ServerWBO("keys").handler()),
"/1.1/johndoe/storage/meta/global": upd("meta", new ServerWBO("global").handler())
@@ -333,17 +334,17 @@ add_task(async function test_sync_partia
} finally {
await cleanAndGo(engine, server);
await engine.finalize();
}
});
add_task(async function test_generic_engine_fail() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
let e = new Error("generic failure message");
engine._errToThrow = e;
@@ -359,17 +360,17 @@ add_task(async function test_generic_eng
});
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_engine_fail_weird_errors() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
try {
let msg = "Bad things happened!";
engine._errToThrow = { message: msg };
@@ -389,17 +390,17 @@ add_task(async function test_engine_fail
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_engine_fail_ioerror() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
// create an IOError to re-throw as part of Sync.
try {
@@ -424,17 +425,17 @@ add_task(async function test_engine_fail
ok(failureReason.error.includes("[profileDir]"), failureReason.error);
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_clean_urls() {
- enableValidationPrefs();
+ forceBookmarkValidation();
Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
engine._errToThrow = new TypeError("http://www.google .com is not a valid URL.");
@@ -456,17 +457,17 @@ add_task(async function test_clean_urls(
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_initial_sync_engines() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
// These are the only ones who actually have things to sync at startup.
let engineNames = ["clients", "bookmarks", "prefs", "tabs"];
let server = await serverForEnginesWithKeys({"foo": "password"}, ["bookmarks", "prefs", "tabs"].map(name =>
Service.engineManager.get(name)
@@ -493,17 +494,17 @@ add_task(async function test_initial_syn
}
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_nserror() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(SteamEngine);
let engine = Service.engineManager.get("steam");
engine.enabled = true;
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
engine._errToThrow = Components.Exception("NS_ERROR_UNKNOWN_HOST", Cr.NS_ERROR_UNKNOWN_HOST);
try {
@@ -521,17 +522,17 @@ add_task(async function test_nserror() {
});
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_discarding() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let helper = track_collections_helper();
let upd = helper.with_updated_collection;
let telem = get_sync_test_telemetry();
telem.maxPayloadCount = 2;
telem.submissionInterval = Infinity;
let oldSubmit = telem.submit;
@@ -568,17 +569,17 @@ add_task(async function test_discarding(
telem.submit = oldSubmit;
if (server) {
await promiseStopServer(server);
}
}
});
add_task(async function test_no_foreign_engines_in_error_ping() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(BogusEngine);
let engine = Service.engineManager.get("bogus");
engine.enabled = true;
let server = await serverForFoo(engine);
engine._errToThrow = new Error("Oh no!");
await SyncTestingInfrastructure(server);
try {
@@ -587,17 +588,17 @@ add_task(async function test_no_foreign_
ok(ping.engines.every(e => e.name !== "bogus"));
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_no_foreign_engines_in_success_ping() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(BogusEngine);
let engine = Service.engineManager.get("bogus");
engine.enabled = true;
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
try {
@@ -605,17 +606,17 @@ add_task(async function test_no_foreign_
ok(ping.engines.every(e => e.name !== "bogus"));
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_events() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(BogusEngine);
let engine = Service.engineManager.get("bogus");
engine.enabled = true;
let server = await serverForFoo(engine);
await SyncTestingInfrastructure(server);
try {
@@ -649,17 +650,17 @@ add_task(async function test_events() {
equal(value, null);
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_invalid_events() {
- enableValidationPrefs();
+ forceBookmarkValidation();
await Service.engineManager.register(BogusEngine);
let engine = Service.engineManager.get("bogus");
engine.enabled = true;
let server = await serverForFoo(engine);
async function checkNotRecorded(...args) {
Service.recordTelemetryEvent.call(args);
@@ -692,17 +693,17 @@ add_task(async function test_invalid_eve
await checkNotRecorded("object", "method", "value", badextra);
} finally {
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
add_task(async function test_no_ping_for_self_hosters() {
- enableValidationPrefs();
+ forceBookmarkValidation();
let telem = get_sync_test_telemetry();
let oldSubmit = telem.submit;
await Service.engineManager.register(BogusEngine);
let engine = Service.engineManager.get("bogus");
engine.enabled = true;
let server = await serverForFoo(engine);
@@ -721,8 +722,118 @@ add_task(async function test_no_ping_for
// so we don't need to do anything to simulate a self-hosted user.
ok(!pingSubmitted, "Should not submit ping with custom token server URL");
} finally {
telem.submit = oldSubmit;
await cleanAndGo(engine, server);
Service.engineManager.unregister(engine);
}
});
+
+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.
+ duration,
+ version: validator.version,
+ recordCount: server.length,
+ problems: problemData,
+ };
+ 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() {
+ // It would be nice not to need to copy these from the repair tests...
+ // But this test can't go there since it needs access to the ping schema,
+ // which isn't un-solvable, but this seemed easier and more worthwhile
+ 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,
+ "annos": [{
+ "name": "Places/SmartBookmark",
+ "flags": 0,
+ "expires": 4,
+ "value": "RecentTags"
+ }],
+ "type": "text/x-moz-place",
+ "uri": "place:type=6&sort=14&maxResults=10"
+ }
+ ]
+ }
+ ]
+ };
+
+ // 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 },
+ ]);
+});
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -1,10 +1,10 @@
[DEFAULT]
-head = head_appinfo.js ../../../common/tests/unit/head_helpers.js head_helpers.js head_http_server.js head_errorhandler_common.js
+head = head_appinfo.js ../../../common/tests/unit/head_helpers.js head_helpers.js head_http_server.js head_errorhandler_common.js ../../../../browser/extensions/sync-repair/test/unit/head.js
firefox-appdir = browser
skip-if = asan # asan crashes, bug 1344085
support-files =
addon1-search.xml
bootstrap1-search.xml
missing-sourceuri.xml
missing-xpi-search.xml
places_v10_from_v11.sqlite
@@ -135,53 +135,40 @@ tags = addons
[test_bookmark_duping.js]
run-sequentially = Frequent timeouts, bug 1395148
[test_bookmark_engine.js]
[test_bookmark_invalid.js]
[test_bookmark_livemarks.js]
[test_bookmark_order.js]
[test_bookmark_places_query_rewriting.js]
[test_bookmark_record.js]
-[test_bookmark_repair.js]
-skip-if = release_or_beta
-run-sequentially = Frequent timeouts, bug 1395148
-[test_bookmark_repair_requestor.js]
-# Repair is enabled only on Aurora and Nightly
-skip-if = release_or_beta
-[test_bookmark_repair_responder.js]
-skip-if = release_or_beta
-run-sequentially = Frequent timeouts, bug 1395148
[test_bookmark_smart_bookmarks.js]
[test_bookmark_store.js]
[test_bookmark_decline_undecline.js]
# Too many intermittent "ASSERTION: thread pool wasn't shutdown: '!mPool'" (bug 804479)
skip-if = debug
[test_bookmark_tracker.js]
requesttimeoutfactor = 4
-[test_bookmark_validator.js]
[test_clients_engine.js]
run-sequentially = Frequent timeouts, bug 1395148
[test_clients_escape.js]
-[test_doctor.js]
[test_extension_storage_engine.js]
[test_extension_storage_tracker.js]
[test_forms_store.js]
[test_forms_tracker.js]
# Too many intermittent "ASSERTION: thread pool wasn't shutdown: '!mPool'" (bug 804479)
skip-if = debug
-[test_form_validator.js]
[test_history_engine.js]
[test_history_store.js]
[test_history_tracker.js]
# Too many intermittent "ASSERTION: thread pool wasn't shutdown: '!mPool'" (bug 804479)
skip-if = debug
[test_places_guid_downgrade.js]
[test_password_engine.js]
[test_password_store.js]
-[test_password_validator.js]
[test_password_tracker.js]
# Too many intermittent "ASSERTION: thread pool wasn't shutdown: '!mPool'" (bug 804479)
skip-if = debug
[test_prefs_store.js]
support-files = prefs_test_prefs_store.js
[test_prefs_tracker.js]
[test_tab_engine.js]
[test_tab_store.js]
--- a/services/sync/tps/extensions/tps/resource/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -24,33 +24,32 @@ Cu.import("resource://gre/modules/Timer.
Cu.import("resource://gre/modules/PromiseUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/main.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/telemetry.js");
-Cu.import("resource://services-sync/bookmark_validator.js");
-Cu.import("resource://services-sync/engines/passwords.js");
-Cu.import("resource://services-sync/engines/forms.js");
-Cu.import("resource://services-sync/engines/addons.js");
// TPS modules
Cu.import("resource://tps/logger.jsm");
// Module wrappers for tests
Cu.import("resource://tps/modules/addons.jsm");
Cu.import("resource://tps/modules/bookmarks.jsm");
Cu.import("resource://tps/modules/forms.jsm");
Cu.import("resource://tps/modules/history.jsm");
Cu.import("resource://tps/modules/passwords.jsm");
Cu.import("resource://tps/modules/prefs.jsm");
Cu.import("resource://tps/modules/tabs.jsm");
Cu.import("resource://tps/modules/windows.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "getValidator",
+ "resource://sync-repair/collection_repair.jsm");
+
var hh = Cc["@mozilla.org/network/protocol;1?name=http"]
.getService(Ci.nsIHttpProtocolHandler);
var prefs = Cc["@mozilla.org/preferences-service;1"]
.getService(Ci.nsIPrefBranch);
XPCOMUtils.defineLazyGetter(this, "fileProtocolHandler", () => {
let fileHandler = Services.io.getProtocolHandler("file");
return fileHandler.QueryInterface(Ci.nsIFileProtocolHandler);
@@ -624,17 +623,17 @@ var TPS = {
Logger.logInfo("About to perform bookmark validation");
let clientTree = await (PlacesUtils.promiseBookmarksTree("", {
includeItemIds: true
}));
let serverRecords = await getServerBookmarkState();
// We can't wait until catch to stringify this, since at that point it will have cycles.
serverRecordDumpStr = JSON.stringify(serverRecords);
- let validator = new BookmarkValidator();
+ let validator = getValidator("bookmarks");
let {problemData} = await validator.compareServerWithClient(serverRecords, clientTree);
for (let {name, count} of problemData.getSummary()) {
// Exclude mobile showing up on the server hackily so that we don't
// report it every time, see bug 1273234 and 1274394 for more information.
if (name === "serverUnexpected" && problemData.serverUnexpected.indexOf("mobile") >= 0) {
--count;
}
@@ -652,23 +651,23 @@ var TPS = {
if (serverRecordDumpStr) {
Logger.logInfo("Server bookmark records:\n" + serverRecordDumpStr + "\n");
}
this.DumpError("Bookmark validation failed", e);
}
Logger.logInfo("Bookmark validation finished");
},
- async ValidateCollection(engineName, ValidatorType) {
+ async ValidateCollection(engineName) {
let serverRecordDumpStr;
let clientRecordDumpStr;
try {
Logger.logInfo(`About to perform validation for "${engineName}"`);
let engine = Weave.Service.engineManager.get(engineName);
- let validator = new ValidatorType(engine);
+ let validator = getValidator(engineName);
let serverRecords = await validator.getServerItems(engine);
let clientRecords = await validator.getClientItems();
try {
// This substantially improves the logs for addons while not making a
// substantial difference for the other two
clientRecordDumpStr = JSON.stringify(clientRecords.map(r => {
let res = validator.normalizeClientItem(r);
delete res.original; // Try and prevent cyclic references
@@ -701,25 +700,25 @@ var TPS = {
Logger.logInfo(`Server state for ${engineName}:\n${serverRecordDumpStr}\n`);
}
this.DumpError(`Validation failed for ${engineName}`, e);
}
Logger.logInfo(`Validation finished for ${engineName}`);
},
ValidatePasswords() {
- return this.ValidateCollection("passwords", PasswordValidator);
+ return this.ValidateCollection("passwords");
},
ValidateForms() {
- return this.ValidateCollection("forms", FormValidator);
+ return this.ValidateCollection("forms");
},
ValidateAddons() {
- return this.ValidateCollection("addons", AddonValidator);
+ return this.ValidateCollection("addons");
},
async RunNextTestAction() {
try {
if (this._currentAction >= this._phaselist[this._currentPhase].length) {
// Run necessary validations and then finish up
if (this.shouldValidateBookmarks) {
await this.ValidateBookmarks();
--- a/tools/lint/eslint/modules.json
+++ b/tools/lint/eslint/modules.json
@@ -1,10 +1,11 @@
{
"AboutHome.jsm": ["AboutHomeUtils", "AboutHome"],
+ "addon_validator.jsm": ["AddonValidator"],
"AddonManager.jsm": ["AddonManager", "AddonManagerPrivate"],
"addons.js": ["AddonsEngine", "AddonValidator"],
"addons.jsm": ["Addon", "STATE_ENABLED", "STATE_DISABLED"],
"addonsreconciler.js": ["AddonsReconciler", "CHANGE_INSTALLED", "CHANGE_UNINSTALLED", "CHANGE_ENABLED", "CHANGE_DISABLED"],
"AddonTestUtils.jsm": ["AddonTestUtils", "MockAsyncShutdown"],
"addonutils.js": ["AddonUtils"],
"ajv-4.1.1.js": ["Ajv"],
"AlertsHelper.jsm": [],
@@ -12,27 +13,27 @@
"AppInfo.jsm": ["newAppInfo", "getAppInfo", "updateAppInfo"],
"async.js": ["Async"],
"AsyncSpellCheckTestHelper.jsm": ["onSpellCheck"],
"AutoMigrate.jsm": ["AutoMigrate"],
"Battery.jsm": ["GetBattery", "Battery"],
"blocklist-clients.js": ["AddonBlocklistClient", "GfxBlocklistClient", "OneCRLBlocklistClient", "PluginBlocklistClient"],
"blocklist-updater.js": ["checkVersions", "addTestBlocklistClient"],
"bogus_element_type.jsm": [],
- "bookmark_repair.js": ["BookmarkRepairRequestor", "BookmarkRepairResponder"],
- "bookmark_validator.js": ["BookmarkValidator", "BookmarkProblemData"],
+ "bookmark_repair.jsm": ["BookmarkRepairRequestor", "BookmarkRepairResponder"],
+ "bookmark_validator.jsm": ["BookmarkValidator", "BookmarkProblemData"],
"bookmarks.js": ["BookmarksEngine", "PlacesItem", "Bookmark", "BookmarkFolder", "BookmarkQuery", "Livemark", "BookmarkSeparator"],
"bookmarks.jsm": ["PlacesItem", "Bookmark", "Separator", "Livemark", "BookmarkFolder", "DumpBookmarks"],
"BootstrapMonitor.jsm": ["monitor"],
"browser-loader.js": ["BrowserLoader"],
"browserid_identity.js": ["BrowserIDManager", "AuthenticationError"],
"CertUtils.jsm": ["BadCertHandler", "checkCert", "readCertPrefs", "validateCert"],
"clients.js": ["ClientEngine", "ClientsRec"],
- "collection_repair.js": ["getRepairRequestor", "getAllRepairRequestors", "CollectionRepairRequestor", "getRepairResponder", "CollectionRepairResponder"],
- "collection_validator.js": ["CollectionValidator", "CollectionProblemData"],
+ "collection_repair.jsm": ["getRepairRequestor", "getAllRepairRequestors", "CollectionRepairRequestor", "getRepairResponder", "CollectionRepairResponder", "getValidator"],
+ "collection_validator.jsm": ["CollectionValidator", "CollectionProblemData"],
"Console.jsm": ["console", "ConsoleAPI"],
"constants.js": ["WEAVE_VERSION", "SYNC_API_VERSION", "USER_API_VERSION", "MISC_API_VERSION", "STORAGE_VERSION", "PREFS_BRANCH", "PWDMGR_HOST", "PWDMGR_PASSWORD_REALM", "PWDMGR_PASSPHRASE_REALM", "PWDMGR_KEYBUNDLE_REALM", "DEFAULT_KEYBUNDLE_NAME", "HMAC_INPUT", "SYNC_KEY_ENCODED_LENGTH", "SYNC_KEY_DECODED_LENGTH", "SYNC_KEY_HYPHENATED_LENGTH", "NO_SYNC_NODE_INTERVAL", "MAX_ERROR_COUNT_BEFORE_BACKOFF", "MAX_IGNORE_ERROR_COUNT", "MINIMUM_BACKOFF_INTERVAL", "MAXIMUM_BACKOFF_INTERVAL", "HMAC_EVENT_INTERVAL", "MASTER_PASSWORD_LOCKED_RETRY_INTERVAL", "DEFAULT_BLOCK_PERIOD", "DEFAULT_GUID_FETCH_BATCH_SIZE", "DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE", "DEFAULT_STORE_BATCH_SIZE", "HISTORY_STORE_BATCH_SIZE", "FORMS_STORE_BATCH_SIZE", "PASSWORDS_STORE_BATCH_SIZE", "ADDONS_STORE_BATCH_SIZE", "APPS_STORE_BATCH_SIZE", "DEFAULT_DOWNLOAD_BATCH_SIZE", "DEFAULT_MAX_RECORD_PAYLOAD_BYTES", "SINGLE_USER_THRESHOLD", "MULTI_DEVICE_THRESHOLD", "SCORE_INCREMENT_SMALL", "SCORE_INCREMENT_MEDIUM", "SCORE_INCREMENT_XLARGE", "SCORE_UPDATE_DELAY", "IDLE_OBSERVER_BACK_DELAY", "URI_LENGTH_MAX", "MAX_HISTORY_UPLOAD", "MAX_HISTORY_DOWNLOAD", "STATUS_OK", "SYNC_FAILED", "LOGIN_FAILED", "SYNC_FAILED_PARTIAL", "CLIENT_NOT_CONFIGURED", "STATUS_DISABLED", "MASTER_PASSWORD_LOCKED", "LOGIN_SUCCEEDED", "SYNC_SUCCEEDED", "ENGINE_SUCCEEDED", "LOGIN_FAILED_NO_USERNAME", "LOGIN_FAILED_NO_PASSWORD", "LOGIN_FAILED_NO_PASSPHRASE", "LOGIN_FAILED_NETWORK_ERROR", "LOGIN_FAILED_SERVER_ERROR", "LOGIN_FAILED_INVALID_PASSPHRASE", "LOGIN_FAILED_LOGIN_REJECTED", "METARECORD_DOWNLOAD_FAIL", "VERSION_OUT_OF_DATE", "DESKTOP_VERSION_OUT_OF_DATE", "SETUP_FAILED_NO_PASSPHRASE", "CREDENTIALS_CHANGED", "ABORT_SYNC_COMMAND", "NO_SYNC_NODE_FOUND", "OVER_QUOTA", "PROLONGED_SYNC_FAILURE", "SERVER_MAINTENANCE", "RESPONSE_OVER_QUOTA", "ENGINE_UPLOAD_FAIL", "ENGINE_DOWNLOAD_FAIL", "ENGINE_UNKNOWN_FAIL", "ENGINE_APPLY_FAIL", "ENGINE_METARECORD_DOWNLOAD_FAIL", "ENGINE_METARECORD_UPLOAD_FAIL", "ENGINE_BATCH_INTERRUPTED", "JPAKE_ERROR_CHANNEL", "JPAKE_ERROR_NETWORK", "JPAKE_ERROR_SERVER", "JPAKE_ERROR_TIMEOUT", "JPAKE_ERROR_INTERNAL", "JPAKE_ERROR_INVALID", "JPAKE_ERROR_NODATA", "JPAKE_ERROR_KEYMISMATCH", "JPAKE_ERROR_WRONGMESSAGE", "JPAKE_ERROR_USERABORT", "JPAKE_ERROR_DELAYUNSUPPORTED", "kSyncNotConfigured", "kSyncMasterPasswordLocked", "kSyncWeaveDisabled", "kSyncNetworkOffline", "kSyncBackoffNotMet", "kFirstSyncChoiceNotMade", "kFirefoxShuttingDown", "FIREFOX_ID", "FENNEC_ID", "SEAMONKEY_ID", "TEST_HARNESS_ID", "MIN_PP_LENGTH", "MIN_PASS_LENGTH", "DEVICE_TYPE_DESKTOP", "DEVICE_TYPE_MOBILE", "SQLITE_MAX_VARIABLE_NUMBER"],
"Constants.jsm": ["Roles", "Events", "Relations", "Filters", "States", "Prefilters"],
"ContactDB.jsm": ["ContactDB", "DB_NAME", "STORE_NAME", "SAVED_GETALL_STORE_NAME", "REVISION_STORE", "DB_VERSION"],
"content-server.jsm": ["init"],
"content.jsm": ["registerContentFrame"],
"ContentCrashHandlers.jsm": ["TabCrashHandler", "PluginCrashReporter", "UnsubmittedCrashHandler"],
"ContentObservers.js": [],
@@ -63,17 +64,18 @@
"ExtensionXPCShellUtils.jsm": ["ExtensionTestUtils"],
"NativeManifests.jsm": ["NativeManifests"],
"fakeservices.js": ["FakeCryptoService", "FakeFilesystemService", "FakeGUIDService", "fakeSHA256HMAC"],
"file_expandosharing.jsm": ["checkFromJSM"],
"file_stringencoding.jsm": ["checkFromJSM"],
"file_url.jsm": ["checkFromJSM"],
"file_worker_url.jsm": ["checkFromJSM"],
"Finder.jsm": ["Finder", "GetClipboardSearchString"],
- "forms.js": ["FormEngine", "FormRec", "FormValidator"],
+ "form_validator.jsm": ["FormValidator", "FormProblemData"],
+ "forms.js": ["FormEngine", "FormRec"],
"forms.jsm": ["FormData"],
"FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"],
"FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"],
"FrameScriptManager.jsm": ["getNewLoaderID"],
"fxa_utils.js": ["initializeIdentityWithTokenServerResponse"],
"fxaccounts.jsm": ["Authentication"],
"FxAccounts.jsm": ["fxAccounts", "FxAccounts"],
"FxAccountsCommon.js": ["log", "logPII", "FXACCOUNTS_PERMISSION", "DATA_FORMAT_VERSION", "DEFAULT_STORAGE_FILENAME", "ASSERTION_LIFETIME", "ASSERTION_USE_PERIOD", "CERT_LIFETIME", "KEY_LIFETIME", "POLL_SESSION", "ONLOGIN_NOTIFICATION", "ONVERIFIED_NOTIFICATION", "ONLOGOUT_NOTIFICATION", "ON_FXA_UPDATE_NOTIFICATION", "ON_DEVICE_CONNECTED_NOTIFICATION", "ON_DEVICE_DISCONNECTED_NOTIFICATION", "ON_PROFILE_UPDATED_NOTIFICATION", "ON_PASSWORD_CHANGED_NOTIFICATION", "ON_PASSWORD_RESET_NOTIFICATION", "ON_VERIFY_LOGIN_NOTIFICATION", "ON_ACCOUNT_DESTROYED_NOTIFICATION", "ON_COLLECTION_CHANGED_NOTIFICATION", "FXA_PUSH_SCOPE_ACCOUNT_UPDATE", "ON_PROFILE_CHANGE_NOTIFICATION", "ON_ACCOUNT_STATE_CHANGE_NOTIFICATION", "UI_REQUEST_SIGN_IN_FLOW", "UI_REQUEST_REFRESH_AUTH", "FX_OAUTH_CLIENT_ID", "WEBCHANNEL_ID", "PREF_LAST_FXA_USER", "ERRNO_ACCOUNT_ALREADY_EXISTS", "ERRNO_ACCOUNT_DOES_NOT_EXIST", "ERRNO_INCORRECT_PASSWORD", "ERRNO_UNVERIFIED_ACCOUNT", "ERRNO_INVALID_VERIFICATION_CODE", "ERRNO_NOT_VALID_JSON_BODY", "ERRNO_INVALID_BODY_PARAMETERS", "ERRNO_MISSING_BODY_PARAMETERS", "ERRNO_INVALID_REQUEST_SIGNATURE", "ERRNO_INVALID_AUTH_TOKEN", "ERRNO_INVALID_AUTH_TIMESTAMP", "ERRNO_MISSING_CONTENT_LENGTH", "ERRNO_REQUEST_BODY_TOO_LARGE", "ERRNO_TOO_MANY_CLIENT_REQUESTS", "ERRNO_INVALID_AUTH_NONCE", "ERRNO_ENDPOINT_NO_LONGER_SUPPORTED", "ERRNO_INCORRECT_LOGIN_METHOD", "ERRNO_INCORRECT_KEY_RETRIEVAL_METHOD", "ERRNO_INCORRECT_API_VERSION", "ERRNO_INCORRECT_EMAIL_CASE", "ERRNO_ACCOUNT_LOCKED", "ERRNO_ACCOUNT_UNLOCKED", "ERRNO_UNKNOWN_DEVICE", "ERRNO_DEVICE_SESSION_CONFLICT", "ERRNO_SERVICE_TEMP_UNAVAILABLE", "ERRNO_PARSE", "ERRNO_NETWORK", "ERRNO_UNKNOWN_ERROR", "OAUTH_SERVER_ERRNO_OFFSET", "ERRNO_UNKNOWN_CLIENT_ID", "ERRNO_INCORRECT_CLIENT_SECRET", "ERRNO_INCORRECT_REDIRECT_URI", "ERRNO_INVALID_FXA_ASSERTION", "ERRNO_UNKNOWN_CODE", "ERRNO_INCORRECT_CODE", "ERRNO_EXPIRED_CODE", "ERRNO_OAUTH_INVALID_TOKEN", "ERRNO_INVALID_REQUEST_PARAM", "ERRNO_INVALID_RESPONSE_TYPE", "ERRNO_UNAUTHORIZED", "ERRNO_FORBIDDEN", "ERRNO_INVALID_CONTENT_TYPE", "ERROR_ACCOUNT_ALREADY_EXISTS", "ERROR_ACCOUNT_DOES_NOT_EXIST", "ERROR_ACCOUNT_LOCKED", "ERROR_ACCOUNT_UNLOCKED", "ERROR_ALREADY_SIGNED_IN_USER", "ERROR_DEVICE_SESSION_CONFLICT", "ERROR_ENDPOINT_NO_LONGER_SUPPORTED", "ERROR_INCORRECT_API_VERSION", "ERROR_INCORRECT_EMAIL_CASE", "ERROR_INCORRECT_KEY_RETRIEVAL_METHOD", "ERROR_INCORRECT_LOGIN_METHOD", "ERROR_INVALID_EMAIL", "ERROR_INVALID_AUDIENCE", "ERROR_INVALID_AUTH_TOKEN", "ERROR_INVALID_AUTH_TIMESTAMP", "ERROR_INVALID_AUTH_NONCE", "ERROR_INVALID_BODY_PARAMETERS", "ERROR_INVALID_PASSWORD", "ERROR_INVALID_VERIFICATION_CODE", "ERROR_INVALID_REFRESH_AUTH_VALUE", "ERROR_INVALID_REQUEST_SIGNATURE", "ERROR_INTERNAL_INVALID_USER", "ERROR_MISSING_BODY_PARAMETERS", "ERROR_MISSING_CONTENT_LENGTH", "ERROR_NO_TOKEN_SESSION", "ERROR_NO_SILENT_REFRESH_AUTH", "ERROR_NOT_VALID_JSON_BODY", "ERROR_OFFLINE", "ERROR_PERMISSION_DENIED", "ERROR_REQUEST_BODY_TOO_LARGE", "ERROR_SERVER_ERROR", "ERROR_SYNC_DISABLED", "ERROR_TOO_MANY_CLIENT_REQUESTS", "ERROR_SERVICE_TEMP_UNAVAILABLE", "ERROR_UI_ERROR", "ERROR_UI_REQUEST", "ERROR_PARSE", "ERROR_NETWORK", "ERROR_UNKNOWN", "ERROR_UNKNOWN_DEVICE", "ERROR_UNVERIFIED_ACCOUNT", "ERROR_UNKNOWN_CLIENT_ID", "ERROR_INCORRECT_CLIENT_SECRET", "ERROR_INCORRECT_REDIRECT_URI", "ERROR_INVALID_FXA_ASSERTION", "ERROR_UNKNOWN_CODE", "ERROR_INCORRECT_CODE", "ERROR_EXPIRED_CODE", "ERROR_OAUTH_INVALID_TOKEN", "ERROR_INVALID_REQUEST_PARAM", "ERROR_INVALID_RESPONSE_TYPE", "ERROR_UNAUTHORIZED", "ERROR_FORBIDDEN", "ERROR_INVALID_CONTENT_TYPE", "ERROR_NO_ACCOUNT", "ERROR_AUTH_ERROR", "ERROR_INVALID_PARAMETER", "ERROR_CODE_METHOD_NOT_ALLOWED", "ERROR_MSG_METHOD_NOT_ALLOWED", "FXA_PWDMGR_PLAINTEXT_FIELDS", "FXA_PWDMGR_SECURE_FIELDS", "FXA_PWDMGR_MEMORY_FIELDS", "FXA_PWDMGR_REAUTH_WHITELIST", "FXA_PWDMGR_HOST", "FXA_PWDMGR_REALM", "SERVER_ERRNO_TO_ERROR", "ERROR_TO_GENERAL_ERROR_CLASS"],
@@ -142,17 +144,18 @@
"osfile_win_allthreads.jsm": ["declareFFI", "libc", "Error", "AbstractInfo", "AbstractEntry", "Type", "POS_START", "POS_CURRENT", "POS_END"],
"ospath_unix.jsm": ["basename", "dirname", "join", "normalize", "split", "toFileURI", "fromFileURI"],
"ospath_win.jsm": ["basename", "dirname", "join", "normalize", "split", "winGetDrive", "winIsAbsolute", "toFileURI", "fromFileURI"],
"OutputGenerator.jsm": ["UtteranceGenerator", "BrailleGenerator"],
"PageMenu.jsm": ["PageMenuParent", "PageMenuChild"],
"PageThumbs.jsm": ["PageThumbs", "PageThumbsStorage"],
"Parser.jsm": ["Parser", "ParserHelpers", "SyntaxTreeVisitor"],
"ParseSymbols.jsm": ["ParseSymbols"],
- "passwords.js": ["PasswordEngine", "LoginRec", "PasswordValidator"],
+ "password_validator.jsm": ["PasswordValidator"],
+ "passwords.js": ["PasswordEngine", "LoginRec"],
"passwords.jsm": ["Password", "DumpPasswords"],
"PdfJsNetwork.jsm": ["NetworkManager"],
"PhoneNumberMetaData.jsm": ["PHONE_NUMBER_META_DATA"],
"PlacesUtils.jsm": ["PlacesUtils", "PlacesAggregatedTransaction", "PlacesCreateFolderTransaction", "PlacesCreateBookmarkTransaction", "PlacesCreateSeparatorTransaction", "PlacesCreateLivemarkTransaction", "PlacesMoveItemTransaction", "PlacesRemoveItemTransaction", "PlacesEditItemTitleTransaction", "PlacesEditBookmarkURITransaction", "PlacesEditLivemarkFeedURITransaction", "PlacesEditLivemarkSiteURITransaction", "PlacesSetItemAnnotationTransaction", "PlacesSetPageAnnotationTransaction", "PlacesEditBookmarkKeywordTransaction", "PlacesEditBookmarkPostDataTransaction", "PlacesEditItemDateAddedTransaction", "PlacesEditItemLastModifiedTransaction", "PlacesSortFolderByNameTransaction", "PlacesTagURITransaction", "PlacesUntagURITransaction"],
"PluginProvider.jsm": [],
"PointerAdapter.jsm": ["PointerRelay", "PointerAdapter"],
"policies.js": ["ErrorHandler", "SyncScheduler"],
"prefs.js": ["PrefsEngine", "PrefRec"],
@@ -164,16 +167,17 @@
"Readability.js": ["Readability"],
"record.js": ["WBORecord", "RecordManager", "CryptoWrapper", "CollectionKeyManager", "Collection"],
"recursive_importA.jsm": ["foo", "bar"],
"recursive_importB.jsm": ["baz", "qux"],
"reflect.jsm": ["Reflect"],
"RemoteFinder.jsm": ["RemoteFinder", "RemoteFinderListener"],
"RemotePageManager.jsm": ["RemotePages", "RemotePageManager", "PageListener"],
"RemoteWebProgress.jsm": ["RemoteWebProgressManager"],
+ "repair_manager.jsm": ["RepairManager"],
"resource.js": ["AsyncResource", "Resource"],
"rest.js": ["RESTRequest", "RESTResponse", "TokenAuthenticatedRESTRequest"],
"rotaryengine.js": ["RotaryEngine", "RotaryRecord", "RotaryStore", "RotaryTracker"],
"require.js": ["require"],
"RTCStatsReport.jsm": ["convertToRTCStatsReport"],
"scratchpad-manager.jsm": ["ScratchpadManager"],
"server.js": ["server"],
"service.js": ["Service"],