--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -7,18 +7,20 @@
ChromeUtils.import("resource://services-sync/main.js");
ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js", {});
});
-ChromeUtils.defineModuleGetter(this, "UIState",
- "resource://services-sync/UIState.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SyncDisconnect: "resource://services-sync/SyncDisconnect.jsm",
+ UIState: "resource://services-sync/UIState.jsm",
+});
const FXA_PAGE_LOGGED_OUT = 0;
const FXA_PAGE_LOGGED_IN = 1;
// Indexes into the "login status" deck.
// We are in a successful verified state - everything should work!
const FXA_LOGIN_VERIFIED = 0;
// We have logged in to an unverified account.
@@ -443,34 +445,30 @@ var gSyncPane = {
};
fxAccounts.resendVerificationEmail()
.then(fxAccounts.getSignedInUser, onError)
.then(onSuccess, onError);
},
unlinkFirefoxAccount(confirm) {
- let doUnlink = () => {
- fxAccounts.signOut().then(() => {
- this.updateWeavePrefs();
- });
- };
if (confirm) {
gSubDialog.open("chrome://browser/content/preferences/in-content/syncDisconnect.xul",
"resizable=no", /* aFeatures */
null, /* aParams */
event => { /* aClosingCallback */
if (event.detail.button == "accept") {
- doUnlink();
+ this.updateWeavePrefs();
}
});
- return;
+ } else {
+ // no confirmation implies no data removal, so just disconnect - but
+ // we still disconnect via the SyncDisconnect module for consistency.
+ SyncDisconnect.disconnect().finally(() => this.updateWeavePrefs());
}
- // no confirmation implies no data removal, so just disconnect.
- doUnlink();
},
_populateComputerName(value) {
let textbox = document.getElementById("fxaSyncComputerName");
if (!textbox.hasAttribute("placeholder")) {
textbox.setAttribute("placeholder",
Weave.Utils.getDefaultDeviceName());
}
--- a/browser/components/preferences/in-content/syncDisconnect.js
+++ b/browser/components/preferences/in-content/syncDisconnect.js
@@ -1,30 +1,45 @@
// 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/.
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ SyncDisconnect: "resource://services-sync/SyncDisconnect.jsm",
+});
+
let gSyncDisconnectDialog = {
init() {
+ if (SyncDisconnect.promiseDisconnectFinished) {
+ // There's a sanitization under way - just re-show our "waiting" state
+ // and close the dialog when it's complete.
+ this.waitForCompletion(SyncDisconnect.promiseDisconnectFinished);
+ }
},
// when either of the checkboxes are changed.
onDeleteOptionChange() {
let eitherChecked = document.getElementById("deleteRemoteSyncData").checked ||
document.getElementById("deleteRemoteOtherData").checked;
let newTitle = eitherChecked ? "sync-disconnect-confirm-disconnect-delete" :
"sync-disconnect-confirm-disconnect";
let butDisconnect = document.getElementById("butDisconnect");
document.l10n.setAttributes(butDisconnect, newTitle);
},
accept(event) {
- // * Check the check-boxes
- // * Start the disconnect and get the completion promise.
- this.waitForCompletion(Promise.resolve());
+ let options = {
+ sanitizeSyncData: document.getElementById("deleteRemoteSyncData").checked,
+ sanitizeBrowserData: document.getElementById("deleteRemoteOtherData").checked,
+ };
+
+ // And do the santize.
+ this.waitForCompletion(SyncDisconnect.disconnect(options));
},
waitForCompletion(promiseComplete) {
// Change the dialog to show we are waiting for completion.
document.getElementById("deleteOptionsContent").hidden = true;
document.getElementById("deletingContent").hidden = false;
// And do the santize.
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -80,14 +80,15 @@ run-if = nightly_build
[browser_security-2.js]
[browser_spotlight.js]
[browser_site_login_exceptions.js]
[browser_permissions_dialog.js]
[browser_subdialogs.js]
support-files =
subdialog.xul
subdialog2.xul
+[browser_sync_sanitize.js]
[browser_telemetry.js]
# Skip this test on Android as FHR and Telemetry are separate systems there.
skip-if = !healthreport || !telemetry || (os == 'linux' && debug) || (os == 'android')
[browser_containers_name_input.js]
run-if = nightly_build # Containers is enabled only on Nightly
[browser_fluent.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_sync_sanitize.js
@@ -0,0 +1,259 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global sinon */
+
+"use strict";
+
+const {UIState} = ChromeUtils.import("resource://services-sync/UIState.jsm", {});
+const {Log} = ChromeUtils.import("resource://gre/modules/Log.jsm", {});
+const {AsyncShutdown} = ChromeUtils.import("resource://gre/modules/AsyncShutdown.jsm", {});
+
+const {SyncDisconnect, SyncDisconnectInternal} = ChromeUtils.import("resource://services-sync/SyncDisconnect.jsm", {});
+
+// Use sinon for mocking.
+Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js");
+registerCleanupFunction(() => {
+ delete window.sinon; // test fails with this reference left behind.
+});
+
+
+add_task(async function setup() {
+ // Sync start-up will interfere with our tests, don't let UIState send UI updates.
+ const origNotifyStateUpdated = UIState._internal.notifyStateUpdated;
+ UIState._internal.notifyStateUpdated = () => {};
+
+ const origGet = UIState.get;
+ UIState.get = () => { return { status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" }; };
+
+ // browser_sync_sanitize uses the sync log, so arrange for that to end up
+ // in the test output.
+ const log = Log.repository.getLogger("Sync");
+ const appender = new Log.DumpAppender();
+ log.addAppender(appender);
+ log.level = appender.level = Log.Level.Trace;
+
+ registerCleanupFunction(() => {
+ UIState._internal.notifyStateUpdated = origNotifyStateUpdated;
+ UIState.get = origGet;
+ });
+});
+
+add_task(async function testDisconnectUI() {
+ await runTestWithSanitizeDialog(async (win, sinon) => {
+ let doc = win.document;
+ let butDisconnect = doc.getElementById("butDisconnect");
+ let butDeleteSync = doc.getElementById("deleteRemoteSyncData");
+ let butDeleteOther = doc.getElementById("deleteRemoteOtherData");
+
+ // mock both sanitize functions and the fxa signout.
+ let spyBrowser = sinon.spy(SyncDisconnectInternal, "doSanitizeBrowserData");
+ let spySync = sinon.spy(SyncDisconnectInternal, "doSanitizeSyncData");
+ let spySignout = sinon.spy(SyncDisconnectInternal, "doSyncAndAccountDisconnect");
+
+ Assert.equal(butDisconnect.getAttribute("data-l10n-id"), "sync-disconnect-confirm-disconnect");
+
+ // Both checkboxes default to unchecked.
+ Assert.ok(!butDeleteSync.checked);
+ Assert.ok(!butDeleteOther.checked);
+
+ // Hitting either of the checkboxes should change the text on the disconnect button/
+ butDeleteSync.click();
+ Assert.ok(butDeleteSync.checked);
+ Assert.equal(butDisconnect.getAttribute("data-l10n-id"), "sync-disconnect-confirm-disconnect-delete");
+
+ butDeleteOther.click();
+ Assert.ok(butDeleteOther.checked);
+ Assert.equal(butDisconnect.getAttribute("data-l10n-id"), "sync-disconnect-confirm-disconnect-delete");
+
+ butDeleteSync.click();
+ Assert.ok(!butDeleteSync.checked);
+ Assert.equal(butDisconnect.getAttribute("data-l10n-id"), "sync-disconnect-confirm-disconnect-delete");
+
+ butDeleteOther.click();
+ Assert.ok(!butDeleteOther.checked);
+ // button text should be back to "just disconnect"
+ Assert.equal(butDisconnect.getAttribute("data-l10n-id"), "sync-disconnect-confirm-disconnect");
+
+ // Cancel the dialog - ensure it closes without sanitizing anything and
+ // without disconnecting FxA.
+ let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload");
+ doc.getElementById("butCancel").click();
+
+ info("waiting for dialog to unload");
+ await promiseUnloaded;
+
+ Assert.equal(spyBrowser.callCount, 0, "should not have sanitized the browser");
+ Assert.equal(spySync.callCount, 0, "should not have sanitized Sync");
+ Assert.equal(spySignout.callCount, 0, "should not have signed out of FxA");
+ });
+});
+
+add_task(async function testDisconnectNoSanitize() {
+ await runTestWithSanitizeDialog(async (win, sinon) => {
+ let doc = win.document;
+ let butDisconnect = doc.getElementById("butDisconnect");
+
+ let spySignout = sinon.spy(SyncDisconnectInternal, "doSyncAndAccountDisconnect");
+ let spySync = sinon.spy(SyncDisconnectInternal, "doSanitizeSyncData");
+ let spyBrowser = sinon.spy(SyncDisconnectInternal, "doSanitizeBrowserData");
+
+ let Weave = {
+ Service: {
+ enabled: true,
+ lock: sinon.stub().returns(true),
+ unlock: sinon.spy(),
+ startOver: sinon.spy(),
+ }
+ };
+ let weaveStub = sinon.stub(SyncDisconnectInternal, "getWeave");
+ weaveStub.returns(Weave);
+
+ let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload");
+ butDisconnect.click();
+ info("waiting for dialog to unload");
+ await promiseUnloaded;
+
+ Assert.equal(Weave.Service.lock.callCount, 1, "should have taken the lock");
+ Assert.equal(Weave.Service.unlock.callCount, 1, "should have unlocked at the end");
+ Assert.equal(Weave.Service.enabled, true, "sync should be enabled");
+ Assert.equal(spySync.callCount, 0, "should not have sanitized sync data");
+ Assert.equal(spySignout.callCount, 1, "should have disconnected");
+ Assert.equal(spyBrowser.callCount, 0, "should not sanitized browser data");
+ Assert.equal(Weave.Service.startOver.callCount, 1, "should have reset sync");
+ });
+});
+
+add_task(async function testSanitizeSync() {
+ await runTestWithSanitizeDialog(async (win, sinon) => {
+ let doc = win.document;
+ let butDisconnect = doc.getElementById("butDisconnect");
+ let butDeleteSync = doc.getElementById("deleteRemoteSyncData");
+
+ SyncDisconnectInternal.lockRetryInterval = 100;
+
+ let spySignout = sinon.spy(SyncDisconnectInternal, "doSyncAndAccountDisconnect");
+
+ // mock the "browser" sanitize function - it should not be called by
+ // this test.
+ let spyBrowser = sinon.spy(SyncDisconnectInternal, "doSanitizeBrowserData");
+ // mock Sync
+ let mockEngine1 = {
+ enabled: true,
+ name: "Test Engine 1",
+ wipeClient: sinon.spy(),
+ };
+ let mockEngine2 = {
+ enabled: false,
+ name: "Test Engine 2",
+ wipeClient: sinon.spy(),
+ };
+
+ let lockStub = sinon.stub();
+ lockStub.onCall(0).returns(false); // first call fails to get the lock.
+ lockStub.onCall(1).returns(true); // second call gets the lock.
+ let Weave = {
+ Service: {
+ enabled: true,
+ startOver: sinon.spy(),
+ lock: lockStub,
+ unlock: sinon.spy(),
+
+ engineManager: {
+ getAll: sinon.stub().returns([mockEngine1, mockEngine2]),
+ },
+ errorHandler: {
+ resetFileLog: sinon.spy(),
+ }
+ }
+ };
+ let weaveStub = sinon.stub(SyncDisconnectInternal, "getWeave");
+ weaveStub.returns(Weave);
+
+ let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload");
+ butDeleteSync.click();
+ butDisconnect.click();
+ info("waiting for dialog to unload");
+ await promiseUnloaded;
+
+ Assert.equal(Weave.Service.lock.callCount, 2, "should have tried the lock twice");
+ Assert.equal(Weave.Service.unlock.callCount, 1, "should have unlocked at the end");
+ Assert.ok(Weave.Service.enabled, "Weave should be enabled");
+ Assert.equal(Weave.Service.errorHandler.resetFileLog.callCount, 1, "should have reset the log");
+ Assert.equal(mockEngine1.wipeClient.callCount, 1, "enabled engine should have been wiped");
+ Assert.equal(mockEngine2.wipeClient.callCount, 0, "disabled engine should not have been wiped");
+ Assert.equal(spyBrowser.callCount, 0, "should not sanitize the browser");
+ Assert.equal(spySignout.callCount, 1, "should have signed out of FxA");
+ Assert.equal(Weave.Service.startOver.callCount, 1, "should have reset sync");
+ });
+});
+
+add_task(async function testSanitizeBrowser() {
+ await runTestWithSanitizeDialog(async (win, sinon) => {
+ let doc = win.document;
+
+ // The dialog should have the main UI visible.
+ Assert.equal(doc.getElementById("deleteOptionsContent").hidden, false);
+ Assert.equal(doc.getElementById("deletingContent").hidden, true);
+
+ let butDisconnect = doc.getElementById("butDisconnect");
+ let butDeleteOther = doc.getElementById("deleteRemoteOtherData");
+
+ let spySignout = sinon.spy(SyncDisconnectInternal, "doSyncAndAccountDisconnect");
+
+ // mock both sanitize functions.
+ let spyBrowser = sinon.spy(SyncDisconnectInternal, "doSanitizeBrowserData");
+ let spySync = sinon.spy(SyncDisconnectInternal, "doSanitizeSyncData");
+
+ let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload");
+ butDeleteOther.click();
+ butDisconnect.click();
+ info("waiting for dialog to unload");
+ await promiseUnloaded;
+
+ Assert.equal(spyBrowser.callCount, 1, "should have sanitized the browser");
+ Assert.equal(spySync.callCount, 0, "should not have sanitized Sync");
+ Assert.equal(spySignout.callCount, 1, "should have signed out of FxA");
+ });
+});
+
+add_task(async function testDisconnectAlreadyRunning() {
+ // Mock the sanitize process to indicate one is already in progress.
+ let resolveExisting;
+ SyncDisconnectInternal.promiseDisconnectFinished =
+ new Promise(resolve => resolveExisting = resolve);
+
+ await runTestWithSanitizeDialog(async (win, sinon) => {
+ let doc = win.document;
+ // The dialog should have "waiting" visible.
+ Assert.equal(doc.getElementById("deleteOptionsContent").hidden, true);
+ Assert.equal(doc.getElementById("deletingContent").hidden, false);
+
+ let promiseUnloaded = BrowserTestUtils.waitForEvent(win, "unload");
+ resolveExisting();
+
+ info("waiting for dialog to unload");
+ await promiseUnloaded;
+ });
+ SyncDisconnectInternal.promiseDisconnectFinished = null;
+});
+
+async function runTestWithSanitizeDialog(test) {
+ await openPreferencesViaOpenPreferencesAPI("paneSync", {leaveOpen: true});
+
+ let doc = gBrowser.contentDocument;
+
+ let promiseSubDialogLoaded =
+ promiseLoadSubDialog("chrome://browser/content/preferences/in-content/syncDisconnect.xul");
+ doc.getElementById("fxaUnlinkButton").doCommand();
+
+ let win = await promiseSubDialogLoaded;
+
+ let ss = sinon.sandbox.create();
+
+ await test(win, ss);
+
+ ss.restore();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+}
+
new file mode 100644
--- /dev/null
+++ b/services/sync/modules/SyncDisconnect.jsm
@@ -0,0 +1,205 @@
+// 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 module provides a facility for disconnecting Sync and FxA, optionally
+// sanitizing profile data as part of the process.
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Log: "resource://gre/modules/Log.jsm",
+ Sanitizer: "resource:///modules/Sanitizer.jsm",
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
+ fxAccounts: "resource://gre/modules/FxAccounts.jsm",
+ setTimeout: "resource://gre/modules/Timer.jsm",
+});
+
+this.EXPORTED_SYMBOLS = ["SyncDisconnect"];
+
+this.SyncDisconnectInternal = {
+ lockRetryInterval: 1000, // wait 1 seconds before trying for the lock again.
+ lockRetryCount: 120, // Try 120 times (==2 mins) before giving up in disgust.
+ promiseDisconnectFinished: null, // If we are sanitizing, a promise for completion.
+
+ // mocked by tests.
+ getWeave() {
+ return ChromeUtils.import("resource://services-sync/main.js", {}).Weave;
+ },
+
+ // Returns a promise that resolves when we are not syncing, waiting until
+ // a current Sync completes if necessary. Resolves with true if we
+ // successfully waited, in which case the sync lock will have been taken to
+ // ensure future syncs don't state, or resolves with false if we gave up
+ // waiting for the sync to complete (in which case we didn't take a lock -
+ // but note that Sync probably remains locked in this case regardless.)
+ async promiseNotSyncing(abortController) {
+ let weave = this.getWeave();
+ let log = Log.repository.getLogger("Sync.Service");
+ // We might be syncing - poll for up to 2 minutes waiting for the lock.
+ // (2 minutes seems extreme, but should be very rare.)
+ return new Promise(resolve => {
+ abortController.signal.onabort = () => {
+ resolve(false);
+ };
+
+ let attempts = 0;
+ let checkLock = () => {
+ if (abortController.signal.aborted) {
+ // We've already resolved, so don't want a new timer to ever start.
+ return;
+ }
+ if (weave.Service.lock()) {
+ resolve(true);
+ return;
+ }
+ attempts += 1;
+ if (attempts >= this.lockRetryCount) {
+ log.error("Gave up waiting for the sync lock - going ahead with sanitize anyway");
+ resolve(false);
+ return;
+ }
+ log.debug("Waiting a couple of seconds to get the sync lock");
+ setTimeout(checkLock, this.lockRetryInterval);
+ };
+ checkLock();
+ });
+ },
+
+ // Sanitize Sync-related data.
+ async doSanitizeSyncData() {
+ let weave = this.getWeave();
+ // Get the sync logger - if stuff goes wrong it can be useful to have that
+ // recorded in the sync logs.
+ let log = Log.repository.getLogger("Sync.Service");
+ log.info("Starting santitize of Sync data");
+ try {
+ // We clobber data for all Sync engines that are enabled.
+ await weave.Service.promiseInitialized;
+ weave.Service.enabled = false;
+
+ log.info("starting actual sanitization");
+ for (let engine of weave.Service.engineManager.getAll()) {
+ if (engine.enabled) {
+ try {
+ log.info("Wiping engine", engine.name);
+ await engine.wipeClient();
+ } catch (ex) {
+ log.error("Failed to wipe engine", ex);
+ }
+ }
+ }
+ log.info("Finished wiping sync data");
+ } catch (ex) {
+ log.error("Failed to sanitize Sync data", ex);
+ console.error("Failed to sanitize Sync data", ex);
+ }
+ try {
+ // ensure any logs we wrote are flushed to disk.
+ await weave.Service.errorHandler.resetFileLog();
+ } catch (ex) {
+ console.log("Failed to flush the Sync log", ex);
+ }
+ },
+
+ // Sanitize all Browser data.
+ async doSanitizeBrowserData() {
+ try {
+ // sanitize everything other than "open windows" (and we don't do that
+ // because it may confuse the user - they probably want to see
+ // about:prefs with the disconnection reflected.
+ let itemsToClear = Object.keys(Sanitizer.items).filter(k => k != "openWindows");
+ await Sanitizer.sanitize(itemsToClear);
+ } catch (ex) {
+ console.error("Failed to sanitize other data", ex);
+ }
+ },
+
+ async doSyncAndAccountDisconnect(shouldUnlock) {
+ // We do a startOver of Sync first - if we do the account first we end
+ // up with Sync configured but FxA not configured, which causes the browser
+ // UI to briefly enter a "needs reauth" state.
+ let Weave = this.getWeave();
+ await Weave.Service.startOver();
+ await fxAccounts.signOut();
+ // Sync may have been disabled if we santized, so re-enable it now or
+ // else the user will be unable to resync should they sign in before a
+ // restart.
+ Weave.Service.enabled = true;
+
+ // and finally, if we managed to get the lock before, we should unlock it
+ // now.
+ if (shouldUnlock) {
+ Weave.Service.unlock();
+ }
+ },
+
+ // Start the sanitization process. Returns a promise that resolves when
+ // the sanitize is complete, and an AbortController which can be used to
+ // abort the process of waiting for a sync to complete.
+ async _startDisconnect(abortController,
+ {sanitizeSyncData = false, sanitizeBrowserData = false} = {}) {
+ // This is a bit convoluted - we want to wait for a sync to finish before
+ // sanitizing, but want to abort that wait if the browser shuts down while
+ // we are waiting (in which case we'll charge ahead anyway).
+ // So we do this by using an AbortController and passing that to the
+ // function that waits for the sync lock - it will immediately resolve
+ // if the abort controller is aborted.
+ let log = Log.repository.getLogger("Sync.Service");
+ log.info("waiting for any existing syncs to complete");
+ let locked = await this.promiseNotSyncing(abortController);
+
+ if (sanitizeSyncData) {
+ await this.doSanitizeSyncData();
+ }
+
+ // We disconnect before sanitizing the browser data - in a worst-case
+ // scenario where the sanitize takes so long that even the shutdown
+ // blocker doesn't allow it to finish, we should still at least be in
+ // a disconnected state on the next startup.
+ log.info("disconnecting account");
+ await this.doSyncAndAccountDisconnect(locked);
+
+ if (sanitizeBrowserData) {
+ await this.doSanitizeBrowserData();
+ }
+
+ },
+
+ async disconnect(options) {
+ if (this.promiseDisconnectFinished) {
+ throw new Error("A disconnect is already in progress");
+ }
+ let abortController = new AbortController();
+ let promiseDisconnectFinished = this._startDisconnect(abortController, options);
+ this.promiseDisconnectFinished = promiseDisconnectFinished;
+ let shutdownBlocker = () => {
+ // oh dear - we are sanitizing (probably stuck waiting for a sync to
+ // complete) and the browser is shutting down. Let's avoid the wait
+ // for sync to complete and continue the process anyway.
+ abortController.abort();
+ return promiseDisconnectFinished;
+ };
+ AsyncShutdown.quitApplicationGranted.addBlocker(
+ "SyncDisconnect: removing requested data",
+ shutdownBlocker);
+
+ // wait for it to finish - hopefully without the blocker being called.
+ await promiseDisconnectFinished;
+ this.promiseDisconnectFinished = null;
+
+ // sanitize worked so remove our blocker - it's a noop if the blocker
+ // did call us.
+ AsyncShutdown.quitApplicationGranted.removeBlocker(shutdownBlocker);
+ },
+};
+
+this.SyncDisconnect = {
+ get promiseDisconnectFinished() {
+ return SyncDisconnectInternal.promiseDisconnectFinished;
+ },
+
+ disconnect(options) {
+ return SyncDisconnectInternal.disconnect(options);
+ }
+};
--- a/services/sync/moz.build
+++ b/services/sync/moz.build
@@ -29,16 +29,17 @@ EXTRA_JS_MODULES['services-sync'] += [
'modules/engines.js',
'modules/keys.js',
'modules/main.js',
'modules/policies.js',
'modules/record.js',
'modules/resource.js',
'modules/service.js',
'modules/status.js',
+ 'modules/SyncDisconnect.jsm',
'modules/SyncedTabs.jsm',
'modules/telemetry.js',
'modules/UIState.jsm',
'modules/util.js',
]
EXTRA_JS_MODULES['services-sync'].engines += [
'modules/engines/addons.js',
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_disconnect_shutdown.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* global sinon */
+
+"use strict";
+
+const {SyncDisconnect, SyncDisconnectInternal} = ChromeUtils.import("resource://services-sync/SyncDisconnect.jsm", {});
+const {AsyncShutdown} = ChromeUtils.import("resource://gre/modules/AsyncShutdown.jsm", {});
+
+add_task(async function test_shutdown_blocker() {
+ let spySignout = sinon.stub(SyncDisconnectInternal, "doSyncAndAccountDisconnect");
+
+ // We don't need to check for the lock regularly as we end up aborting the wait.
+ SyncDisconnectInternal.lockRetryInterval = 1000;
+ // Force the retry count to a very large value - this test should never
+ // abort due to the retry count and we want the test to fail (aka timeout)
+ // should our abort code not work.
+ SyncDisconnectInternal.lockRetryCount = 10000;
+
+ // mock the "browser" sanitize function - it should not be called by
+ // this test.
+ let spyBrowser = sinon.stub(SyncDisconnectInternal, "doSanitizeBrowserData");
+ // mock Sync
+ let mockEngine1 = {
+ enabled: true,
+ name: "Test Engine 1",
+ wipeClient: sinon.spy(),
+ };
+ let mockEngine2 = {
+ enabled: false,
+ name: "Test Engine 2",
+ wipeClient: sinon.spy(),
+ };
+
+ // This weave mock never gives up the lock.
+ let Weave = {
+ Service: {
+ enabled: true,
+ lock: () => false, // so we never get the lock.
+ unlock: sinon.spy(),
+
+ engineManager: {
+ getAll: sinon.stub().returns([mockEngine1, mockEngine2]),
+ },
+ errorHandler: {
+ resetFileLog: sinon.spy(),
+ }
+ }
+ };
+ let weaveStub = sinon.stub(SyncDisconnectInternal, "getWeave");
+ weaveStub.returns(Weave);
+
+ let promiseDisconnected = SyncDisconnect.disconnect({sanitizeSyncData: true});
+
+ // Pretend we hit the shutdown blocker.
+ info("simulating quitApplicationGranted");
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ AsyncShutdown.quitApplicationGranted._trigger();
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+
+ info("waiting for disconnect to complete");
+ await promiseDisconnected;
+
+ Assert.equal(Weave.Service.unlock.callCount, 0, "should not have unlocked at the end");
+ Assert.ok(!Weave.Service.enabled, "Weave should be and remain disabled");
+ Assert.equal(Weave.Service.errorHandler.resetFileLog.callCount, 1, "should have reset the log");
+ Assert.equal(mockEngine1.wipeClient.callCount, 1, "enabled engine should have been wiped");
+ Assert.equal(mockEngine2.wipeClient.callCount, 0, "disabled engine should not have been wiped");
+ Assert.equal(spyBrowser.callCount, 0, "should not sanitize the browser");
+ Assert.equal(spySignout.callCount, 1, "should have signed out of FxA");
+});
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -191,8 +191,9 @@ support-files = prefs_test_prefs_store.j
# Synced tabs.
[test_syncedtabs.js]
[test_telemetry.js]
requesttimeoutfactor = 4
[test_uistate.js]
[test_412.js]
+[test_disconnect_shutdown.js]