commit 9d5a9962ffade21615dab330126a387ee6496fb5
parent 393d6e23e9653e1e48310ad31c53dd6b9b4896c2
Author: Sammy Khamis <skhamis@mozilla.com>
Date: Fri, 21 Nov 2025 22:50:59 +0000
Bug 2000738 - Update Sync UI to handle third-party auth r=markh,sync-reviewers,Gijs
Differential Revision: https://phabricator.services.mozilla.com/D272967
Diffstat:
10 files changed, 453 insertions(+), 22 deletions(-)
diff --git a/browser/base/content/browser-sync.js b/browser/base/content/browser-sync.js
@@ -5,6 +5,7 @@
const {
FX_MONITOR_OAUTH_CLIENT_ID,
FX_RELAY_OAUTH_CLIENT_ID,
+ SCOPE_APP_SYNC,
VPN_OAUTH_CLIENT_ID,
} = ChromeUtils.importESModule(
"resource://gre/modules/FxAccountsCommon.sys.mjs"
@@ -501,8 +502,10 @@ var gSync = {
// Returns true if FxA is configured, but the send tab targets list isn't
// ready yet.
get sendTabConfiguredAndLoading() {
+ const state = UIState.get();
return (
- UIState.get().status == UIState.STATUS_SIGNED_IN &&
+ state.status == UIState.STATUS_SIGNED_IN &&
+ state.syncEnabled &&
!fxAccounts.device.recentDeviceList
);
},
@@ -527,8 +530,10 @@ var gSync = {
getSendTabTargets() {
const targets = [];
+ const state = UIState.get();
if (
- UIState.get().status != UIState.STATUS_SIGNED_IN ||
+ state.status != UIState.STATUS_SIGNED_IN ||
+ !state.syncEnabled ||
!fxAccounts.device.recentDeviceList
) {
return targets;
@@ -798,7 +803,7 @@ var gSync = {
this.openPrefsFromFxaMenu("sync_settings", button);
break;
case "PanelUI-fxa-menu-setup-sync-button":
- this.openChooseWhatToSync("sync_settings", button);
+ this.openSyncSetup("sync_settings", button);
break;
case "PanelUI-fxa-menu-sendtab-connect-device-button":
@@ -830,7 +835,7 @@ var gSync = {
this.openVPNLink(button);
break;
case "PanelUI-fxa-menu-sendtab-not-configured-button":
- this.openPrefsFromFxaMenu("send_tab", button);
+ this.openSyncSetup("send_tab", button);
break;
}
},
@@ -950,8 +955,8 @@ var gSync = {
},
showSendToDeviceViewFromFxaMenu(anchor) {
- const { status } = UIState.get();
- if (status === UIState.STATUS_NOT_CONFIGURED) {
+ const state = UIState.get();
+ if (state.status !== UIState.STATUS_SIGNED_IN || !state.syncEnabled) {
PanelUI.showSubView("PanelUI-fxa-menu-sendtab-not-configured", anchor);
return;
}
@@ -2302,6 +2307,41 @@ var gSync = {
this.openPrefs(entryPoint, null, { action: "choose-what-to-sync" });
},
+ /**
+ * Opens the appropriate sync setup flow based on whether the user has sync keys.
+ * - If the user has sync keys: opens sync preferences to configure what to sync
+ * - If the user doesn't have sync keys (third-party auth): opens FxA to create password
+ */
+ async openSyncSetup(type, sourceElement, extraParams = {}) {
+ this.emitFxaToolbarTelemetry(type, sourceElement);
+ const entryPoint = this._getEntryPointForElement(sourceElement);
+
+ try {
+ // Check if the user has sync keys
+ const hasKeys = await fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC);
+
+ if (hasKeys) {
+ // User has keys - go to prefs to configure what to sync
+ this.openPrefs(entryPoint, null, { action: "choose-what-to-sync" });
+ } else {
+ // User doesn't have keys (third-party auth) - go to FxA to create password
+ // This will request SCOPE_APP_SYNC so FxA knows to generate sync keys
+ if (!(await FxAccounts.canConnectAccount())) {
+ return;
+ }
+ const url = await FxAccounts.config.promiseConnectAccountURI(
+ entryPoint,
+ extraParams
+ );
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ }
+ } catch (err) {
+ this.log.error("Failed to determine sync setup flow", err);
+ // Fall back to opening prefs
+ this.openPrefs(entryPoint);
+ }
+ },
+
openSyncedTabsPanel() {
let placement = CustomizableUI.getPlacementOfWidget("sync-button");
let area = placement?.area;
diff --git a/browser/components/customizableui/test/browser_synced_tabs_menu.js b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -57,13 +57,28 @@ let mockedInternal = {
add_setup(async function () {
const getSignedInUser = FxAccounts.config.getSignedInUser;
+
FxAccounts.config.getSignedInUser = async () =>
Promise.resolve({ uid: "uid", email: "foo@bar.com" });
+
Services.prefs.setCharPref(
"identity.fxaccounts.remote.root",
"https://example.com/"
);
+ // Mock the global fxAccounts object used by gSync
+ const origWindowFxAccounts = window.fxAccounts;
+ window.fxAccounts = {
+ getSignedInUser: async () => ({ uid: "uid", email: "foo@bar.com" }),
+ hasLocalSession: async () => true,
+ keys: {
+ canGetKeyForScope: async () => true,
+ },
+ device: {
+ recentDeviceList: null,
+ },
+ };
+
let oldInternal = SyncedTabs._internal;
SyncedTabs._internal = mockedInternal;
@@ -78,6 +93,7 @@ add_setup(async function () {
FxAccounts.config.getSignedInUser = getSignedInUser;
Services.prefs.clearUserPref("identity.fxaccounts.remote.root");
UIState._internal.notifyStateUpdated = origNotifyStateUpdated;
+ window.fxAccounts = origWindowFxAccounts;
SyncedTabs._internal = oldInternal;
});
});
diff --git a/browser/components/preferences/sync.js b/browser/components/preferences/sync.js
@@ -4,6 +4,10 @@
/* import-globals-from preferences.js */
+const { SCOPE_APP_SYNC } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccountsCommon.sys.mjs"
+);
+
const FXA_PAGE_LOGGED_OUT = 0;
const FXA_PAGE_LOGGED_IN = 1;
@@ -265,8 +269,31 @@ var gSyncPane = {
document.getElementById("fxaCancelChangeDeviceName").click();
}
});
- setEventListener("syncSetup", "command", function () {
- this._chooseWhatToSync(false, "setupSync");
+ setEventListener("syncSetup", "command", async function () {
+ // Check if the user has sync keys before opening CWTS
+ try {
+ const hasKeys = await fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC);
+ if (hasKeys) {
+ // User has keys - open the choose what to sync dialog
+ this._chooseWhatToSync(false, "setupSync");
+ } else {
+ // User signed in via third-party auth without sync keys.
+ // Redirect to FxA to create a password and generate sync keys.
+ // canConnectAccount() checks if the Primary Password is locked and
+ // prompts the user to unlock it. Returns false if the user cancels.
+ if (!(await FxAccounts.canConnectAccount())) {
+ return;
+ }
+ const url = await FxAccounts.config.promiseConnectAccountURI(
+ this._getEntryPoint()
+ );
+ this.replaceTabWithUrl(url);
+ }
+ } catch (err) {
+ console.error("Failed to check for sync keys", err);
+ // Fallback to opening CWTS dialog
+ this._chooseWhatToSync(false, "setupSync");
+ }
});
setEventListener("syncChangeOptions", "command", function () {
this._chooseWhatToSync(true, "manageSyncSettings");
diff --git a/services/fxaccounts/FxAccountsKeys.sys.mjs b/services/fxaccounts/FxAccountsKeys.sys.mjs
@@ -100,6 +100,33 @@ export class FxAccountsKeys {
}
/**
+ * Checks if we currently have the key for a given scope locally available.
+ *
+ * This method only checks if the keys exist in local storage. With OAuth-based
+ * authentication, keys cannot be fetched on demand - if they don't exist locally,
+ * there is no way to obtain them.
+ *
+ * @param {string} scope The OAuth scope whose key should be checked
+ *
+ * @return Promise<boolean>
+ * Resolves to true if the key exists locally, false otherwise.
+ */
+ hasKeysForScope(scope) {
+ return this._fxai.withCurrentAccountState(async currentState => {
+ let userData = await currentState.getUserAccountData();
+ if (!userData) {
+ return false;
+ }
+ if (!userData.verified) {
+ return false;
+ }
+ return !!(
+ userData.scopedKeys && userData.scopedKeys.hasOwnProperty(scope)
+ );
+ });
+ }
+
+ /**
* Get the key for a specified OAuth scope.
*
* @param {string} scope The OAuth scope whose key should be returned
diff --git a/services/fxaccounts/FxAccountsOAuth.sys.mjs b/services/fxaccounts/FxAccountsOAuth.sys.mjs
@@ -13,6 +13,7 @@ import {
SCOPE_PROFILE,
SCOPE_PROFILE_WRITE,
SCOPE_APP_SYNC,
+ log,
} from "resource://gre/modules/FxAccountsCommon.sys.mjs";
const VALID_SCOPES = [SCOPE_PROFILE, SCOPE_PROFILE_WRITE, SCOPE_APP_SYNC];
@@ -202,11 +203,10 @@ export class FxAccountsOAuth {
);
const requestedSync = requestedScopes.includes(SCOPE_APP_SYNC);
const grantedSync = scope.includes(SCOPE_APP_SYNC);
+ // This is not necessarily unexpected as the user could be using
+ // third-party auth but sent the sync scope, we shouldn't error here
if (requestedSync && !grantedSync) {
- throw new Error(ERROR_SYNC_SCOPE_NOT_GRANTED);
- }
- if (grantedSync && !keys_jwe) {
- throw new Error(ERROR_NO_KEYS_JWE);
+ log.info("Requested Sync scope but was not granted sync!");
}
let scopedKeys;
if (keys_jwe) {
diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -412,8 +412,9 @@ add_test(function test_getKeyForScope() {
add_task(async function test_oauth_verification() {
let fxa = new MockFxAccounts();
- let user = getTestUser("foo");
+ let user = getTestUser("eusebius");
user.verified = false;
+
await fxa.setSignedInUser(user);
let fetched = await fxa.getSignedInUser();
Assert.ok(!fetched.verified);
@@ -423,7 +424,7 @@ add_task(async function test_oauth_verification() {
});
fetched = await fxa.getSignedInUser();
- Assert.ok(!fetched.verified); // keys alone don’t flip verification
+ Assert.ok(!fetched.verified);
// Simulate the follow-up login message that marks the account verified.
await fxa._internal.updateUserAccountData({
@@ -435,6 +436,69 @@ add_task(async function test_oauth_verification() {
Assert.ok(fetched.verified);
});
+// Tests for hasKeysForScope - checking if sync keys exist locally
+add_task(async function test_hasKeysForScope_not_signed_in() {
+ const fxa = await MakeFxAccounts();
+ // Should return false when no user is signed in
+ Assert.ok(!(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC)));
+});
+
+add_task(async function test_hasKeysForScope_not_verified() {
+ const credentials = {
+ email: "foo@example.com",
+ uid: "1234567890abcdef1234567890abcdef",
+ sessionToken: "dead",
+ verified: false, // Not verified
+ ...MOCK_ACCOUNT_KEYS,
+ };
+ const fxa = await MakeFxAccounts({ credentials });
+ // Should return false when user is not verified
+ Assert.ok(!(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC)));
+});
+
+add_task(async function test_hasKeysForScope_no_keys() {
+ const credentials = {
+ email: "foo@example.com",
+ uid: "1234567890abcdef1234567890abcdef",
+ sessionToken: "dead",
+ verified: true,
+ // NO scopedKeys - third party auth scenario
+ };
+ const fxa = await MakeFxAccounts({ credentials });
+ // Should return false when user has no sync keys (third-party auth)
+ Assert.ok(!(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC)));
+});
+
+add_task(async function test_hasKeysForScope_with_keys() {
+ const credentials = {
+ email: "foo@example.com",
+ uid: "1234567890abcdef1234567890abcdef",
+ sessionToken: "dead",
+ verified: true,
+ ...MOCK_ACCOUNT_KEYS, // Has sync keys
+ };
+ const fxa = await MakeFxAccounts({ credentials });
+ // Should return true when user has sync keys
+ Assert.ok(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC));
+});
+
+add_task(async function test_hasKeysForScope_wrong_scope() {
+ const credentials = {
+ email: "foo@example.com",
+ uid: "1234567890abcdef1234567890abcdef",
+ sessionToken: "dead",
+ verified: true,
+ ...MOCK_ACCOUNT_KEYS,
+ };
+ const fxa = await MakeFxAccounts({ credentials });
+ // Should return false for a scope we don't have keys for
+ Assert.ok(
+ !(await fxa.keys.hasKeysForScope(
+ "https://identity.mozilla.com/apps/unknown"
+ ))
+ );
+});
+
add_task(
async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_high_level_keys() {
let fxa = new MockFxAccounts();
diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_flow.js b/services/fxaccounts/tests/xpcshell/test_oauth_flow.js
@@ -10,8 +10,6 @@ const {
ERROR_INVALID_SCOPES,
ERROR_INVALID_SCOPED_KEYS,
ERROR_INVALID_STATE,
- ERROR_SYNC_SCOPE_NOT_GRANTED,
- ERROR_NO_KEYS_JWE,
ERROR_OAUTH_FLOW_ABANDONED,
} = ChromeUtils.importESModule(
"resource://gre/modules/FxAccountsOAuth.sys.mjs"
@@ -125,11 +123,10 @@ add_task(function test_complete_oauth_flow() {
const queryParams = await oauth.beginOAuthFlow(scopes);
try {
await oauth.completeOAuthFlow(sessionToken, "foo", queryParams.state);
+ } catch (err) {
Assert.fail(
- "Should have thrown an error because the sync scope was not authorized"
+ "We should throw if we don't receive requested scope, we skip sync setup"
);
- } catch (err) {
- Assert.equal(err.message, ERROR_SYNC_SCOPE_NOT_GRANTED);
}
});
add_task(async function test_jwe_not_returned() {
@@ -147,11 +144,10 @@ add_task(function test_complete_oauth_flow() {
const sessionToken = "01abcef12";
try {
await oauth.completeOAuthFlow(sessionToken, "foo", queryParams.state);
+ } catch (err) {
Assert.fail(
- "Should have thrown an error because we didn't get back a keys_nwe"
+ "Should not have thrown an error because we didn't get back a keys_jwe,we instead skip sync setup"
);
- } catch (err) {
- Assert.equal(err.message, ERROR_NO_KEYS_JWE);
}
});
add_task(async function test_complete_oauth_ok() {
diff --git a/services/sync/modules/service.sys.mjs b/services/sync/modules/service.sys.mjs
@@ -62,6 +62,7 @@ ChromeUtils.importESModule("resource://services-sync/telemetry.sys.mjs");
import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
+import { SCOPE_APP_SYNC } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
const fxAccounts = getFxAccountsSingleton();
@@ -966,6 +967,22 @@ Sync11Service.prototype = {
}
},
+ // Checks if sync can be configured for the current FxA user.
+ // Returns true if there is a signed-in user with sync keys available.
+ async canConfigure() {
+ let user = await fxAccounts.getSignedInUser();
+ if (!user) {
+ return false;
+ }
+ try {
+ let hasKeys = await fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC);
+ return hasKeys;
+ } catch (err) {
+ this._log.error("Failed to check for sync keys", err);
+ return false;
+ }
+ },
+
// configures/enabled/turns-on sync. There must be an FxA user signed in.
async configure() {
// We don't, and must not, throw if sync is already configured, because we
@@ -976,6 +993,12 @@ Sync11Service.prototype = {
if (!user) {
throw new Error("No FxA user is signed in");
}
+ // Check if the user has sync keys. With OAuth-based authentication,
+ // keys cannot be fetched on demand - they must exist locally.
+ let hasKeys = await fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC);
+ if (!hasKeys) {
+ throw new Error("User does not have sync keys");
+ }
this._log.info("Configuring sync with current FxA user");
Svc.PrefBranch.setStringPref("username", user.email);
Svc.Obs.notify("weave:connected");
diff --git a/services/sync/tests/unit/test_service_attributes.js b/services/sync/tests/unit/test_service_attributes.js
@@ -90,3 +90,169 @@ add_test(function test_locked() {
Assert.equal(Service.locked, false);
run_next_test();
});
+
+// Tests for canConfigure and configure with sync keys
+add_task(async function test_canConfigure_no_user() {
+ _("canConfigure returns false when no user is signed in");
+ const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ );
+ const fxAccounts = getFxAccountsSingleton();
+
+ // Mock getSignedInUser to return null
+ const originalGetSignedInUser = fxAccounts.getSignedInUser;
+ fxAccounts.getSignedInUser = () => Promise.resolve(null);
+
+ try {
+ const canConfigure = await Service.canConfigure();
+ Assert.equal(canConfigure, false);
+ } finally {
+ fxAccounts.getSignedInUser = originalGetSignedInUser;
+ }
+});
+
+add_task(async function test_canConfigure_no_keys() {
+ _("canConfigure returns false when user has no sync keys");
+ const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ );
+ const fxAccounts = getFxAccountsSingleton();
+
+ // Mock getSignedInUser to return a user
+ const originalGetSignedInUser = fxAccounts.getSignedInUser;
+ fxAccounts.getSignedInUser = () =>
+ Promise.resolve({ email: "test@example.com", uid: "12345" });
+
+ // Mock hasKeysForScope to return false
+ const originalHasKeysForScope = fxAccounts.keys.hasKeysForScope;
+ fxAccounts.keys.hasKeysForScope = () => Promise.resolve(false);
+
+ try {
+ const canConfigure = await Service.canConfigure();
+ Assert.equal(canConfigure, false);
+ } finally {
+ fxAccounts.getSignedInUser = originalGetSignedInUser;
+ fxAccounts.keys.hasKeysForScope = originalHasKeysForScope;
+ }
+});
+
+add_task(async function test_canConfigure_with_keys() {
+ _("canConfigure returns true when user has sync keys");
+ const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ );
+ const fxAccounts = getFxAccountsSingleton();
+
+ // Mock getSignedInUser to return a user
+ const originalGetSignedInUser = fxAccounts.getSignedInUser;
+ fxAccounts.getSignedInUser = () =>
+ Promise.resolve({ email: "test@example.com", uid: "12345" });
+
+ // Mock hasKeysForScope to return true
+ const originalHasKeysForScope = fxAccounts.keys.hasKeysForScope;
+ fxAccounts.keys.hasKeysForScope = () => Promise.resolve(true);
+
+ try {
+ const canConfigure = await Service.canConfigure();
+ Assert.equal(canConfigure, true);
+ } finally {
+ fxAccounts.getSignedInUser = originalGetSignedInUser;
+ fxAccounts.keys.hasKeysForScope = originalHasKeysForScope;
+ }
+});
+
+add_task(async function test_configure_throws_without_keys() {
+ _("configure() throws when user has no sync keys");
+ const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ );
+ const fxAccounts = getFxAccountsSingleton();
+
+ // Mock getSignedInUser to return a user
+ const originalGetSignedInUser = fxAccounts.getSignedInUser;
+ fxAccounts.getSignedInUser = () =>
+ Promise.resolve({ email: "test@example.com", uid: "12345" });
+
+ // Mock hasKeysForScope to return false (no keys)
+ const originalHasKeysForScope = fxAccounts.keys.hasKeysForScope;
+ fxAccounts.keys.hasKeysForScope = () => Promise.resolve(false);
+
+ try {
+ await Assert.rejects(
+ Service.configure(),
+ /User does not have sync keys/,
+ "configure() should throw when no sync keys"
+ );
+ } finally {
+ fxAccounts.getSignedInUser = originalGetSignedInUser;
+ fxAccounts.keys.hasKeysForScope = originalHasKeysForScope;
+ }
+});
+
+add_task(async function test_configure_succeeds_with_keys() {
+ _("configure() succeeds when user has sync keys");
+ const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ );
+ const fxAccounts = getFxAccountsSingleton();
+
+ // Mock getSignedInUser to return a user
+ const originalGetSignedInUser = fxAccounts.getSignedInUser;
+ fxAccounts.getSignedInUser = () =>
+ Promise.resolve({ email: "test@example.com", uid: "12345" });
+
+ // Mock hasKeysForScope to return true (has keys)
+ const originalHasKeysForScope = fxAccounts.keys.hasKeysForScope;
+ fxAccounts.keys.hasKeysForScope = () => Promise.resolve(true);
+
+ try {
+ await Service.configure();
+ // Should set the username pref
+ Assert.equal(Svc.PrefBranch.getStringPref("username"), "test@example.com");
+ } finally {
+ fxAccounts.getSignedInUser = originalGetSignedInUser;
+ fxAccounts.keys.hasKeysForScope = originalHasKeysForScope;
+ Svc.PrefBranch.clearUserPref("username");
+ }
+});
+
+add_task(async function test_third_party_to_sync_complete_flow() {
+ _("End-to-end: third-party auth (no keys) -> receive keys -> configure sync");
+ const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ );
+ const fxAccounts = getFxAccountsSingleton();
+
+ // Save originals
+ const originalGetSignedInUser = fxAccounts.getSignedInUser;
+ const originalHasKeysForScope = fxAccounts.keys.hasKeysForScope;
+
+ // 1. User signs in without keys (third-party)
+ const user = { email: "foo@example.com", uid: "uid12345" };
+ fxAccounts.getSignedInUser = () => Promise.resolve(user);
+
+ // 2. Initially no keys - hasKeysForScope returns false
+ fxAccounts.keys.hasKeysForScope = () => Promise.resolve(false);
+
+ try {
+ // 3. Verify sync cannot be configured without keys
+ Assert.ok(!(await Service.canConfigure()));
+
+ // 4. Simulate receiving keys via webchannel (third-party creates password)
+ // Now hasKeysForScope returns true
+ fxAccounts.keys.hasKeysForScope = () => Promise.resolve(true);
+
+ // 5. Verify sync can now be configured
+ Assert.ok(await Service.canConfigure());
+
+ // 6. Configure sync
+ await Service.configure();
+
+ // 7. Verify username pref is set
+ Assert.equal(Svc.PrefBranch.getStringPref("username"), "foo@example.com");
+ } finally {
+ fxAccounts.getSignedInUser = originalGetSignedInUser;
+ fxAccounts.keys.hasKeysForScope = originalHasKeysForScope;
+ Svc.PrefBranch.clearUserPref("username");
+ }
+});
diff --git a/services/sync/tests/unit/test_uistate.js b/services/sync/tests/unit/test_uistate.js
@@ -59,6 +59,7 @@ add_task(async function test_refreshState_signedin() {
const now = new Date().toString();
Services.prefs.setStringPref("services.sync.lastSync", now);
+ Services.prefs.setStringPref("services.sync.username", "foo@bar.com");
UIStateInternal.syncing = false;
UIStateInternal.fxAccounts = {
@@ -71,6 +72,9 @@ add_task(async function test_refreshState_signedin() {
avatar: "https://foo/bar",
}),
hasLocalSession: () => Promise.resolve(true),
+ keys: {
+ canGetKeyForScope: () => Promise.resolve(true),
+ },
};
let state = await UIState.refresh();
@@ -84,6 +88,7 @@ add_task(async function test_refreshState_signedin() {
equal(state.syncing, false);
UIStateInternal.fxAccounts = fxAccountsOrig;
+ Services.prefs.clearUserPref("services.sync.username");
});
add_task(async function test_refreshState_syncButNoFxA() {
@@ -127,6 +132,9 @@ add_task(async function test_refreshState_signedin_profile_unavailable() {
getSignedInUser: () =>
Promise.resolve({ verified: true, uid: "123", email: "foo@bar.com" }),
hasLocalSession: () => Promise.resolve(true),
+ keys: {
+ canGetKeyForScope: () => Promise.resolve(true),
+ },
_internal: {
profile: {
getProfile: () => {
@@ -206,6 +214,9 @@ add_task(async function test_refreshState_loginFailed() {
UIStateInternal.fxAccounts = {
getSignedInUser: () =>
Promise.resolve({ verified: true, uid: "123", email: "foo@bar.com" }),
+ keys: {
+ canGetKeyForScope: () => Promise.resolve(true),
+ },
};
let state = await UIState.refresh();
@@ -258,6 +269,9 @@ async function configureUIState(syncing, lastSync = new Date()) {
getSignedInUser: () =>
Promise.resolve({ verified: true, uid: "123", email: "foo@bar.com" }),
hasLocalSession: () => Promise.resolve(true),
+ keys: {
+ canGetKeyForScope: () => Promise.resolve(true),
+ },
};
await UIState.refresh();
UIStateInternal.fxAccounts = fxAccountsOrig;
@@ -312,6 +326,64 @@ add_task(async function test_syncError() {
deepEqual(newState.lastSync, oldState.lastSync);
});
+add_task(async function test_refreshState_signedin_with_synckeys() {
+ UIState.reset();
+ const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+ Services.prefs.setStringPref("services.sync.username", "test@test.com");
+
+ UIStateInternal.fxAccounts = {
+ getSignedInUser: () =>
+ Promise.resolve({
+ verified: true,
+ uid: "123",
+ email: "foo@bar.com",
+ displayName: "Foo Bar",
+ }),
+ hasLocalSession: () => Promise.resolve(true),
+ keys: {
+ canGetKeyForScope: () => Promise.resolve(true),
+ },
+ };
+
+ let state = await UIState.refresh();
+
+ equal(state.status, UIState.STATUS_SIGNED_IN);
+ equal(state.syncEnabled, true);
+ equal(state.uid, "123");
+ equal(state.email, "foo@bar.com");
+
+ UIStateInternal.fxAccounts = fxAccountsOrig;
+ Services.prefs.clearUserPref("services.sync.username");
+});
+
+// Testing third-party auth does not enable sync (should be impossible without keys)
+add_task(async function test_refreshState_third_party_auth_no_sync() {
+ UIState.reset();
+ const fxAccountsOrig = UIStateInternal.fxAccounts;
+
+ // Mock signing in but NO keys (third-party auth)
+ UIStateInternal.fxAccounts = {
+ getSignedInUser: () =>
+ Promise.resolve({
+ verified: true,
+ uid: "123",
+ email: "foo@bar.com",
+ }),
+ hasLocalSession: () => Promise.resolve(true),
+ keys: {},
+ };
+
+ let state = await UIState.refresh();
+
+ equal(state.status, UIState.STATUS_SIGNED_IN);
+ equal(state.syncEnabled, false);
+ equal(state.uid, "123");
+ equal(state.email, "foo@bar.com");
+
+ UIStateInternal.fxAccounts = fxAccountsOrig;
+});
+
function observeUIUpdate() {
return new Promise(resolve => {
let obs = (aSubject, aTopic) => {