tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbrowser/base/content/browser-sync.js | 52++++++++++++++++++++++++++++++++++++++++++++++------
Mbrowser/components/customizableui/test/browser_synced_tabs_menu.js | 16++++++++++++++++
Mbrowser/components/preferences/sync.js | 31+++++++++++++++++++++++++++++--
Mservices/fxaccounts/FxAccountsKeys.sys.mjs | 27+++++++++++++++++++++++++++
Mservices/fxaccounts/FxAccountsOAuth.sys.mjs | 8++++----
Mservices/fxaccounts/tests/xpcshell/test_accounts.js | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mservices/fxaccounts/tests/xpcshell/test_oauth_flow.js | 12++++--------
Mservices/sync/modules/service.sys.mjs | 23+++++++++++++++++++++++
Mservices/sync/tests/unit/test_service_attributes.js | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mservices/sync/tests/unit/test_uistate.js | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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) => {