diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream/1%115.7.0.tar.xz thunderbird-upstream/1%115.7.0.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js')
-rw-r--r-- | services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js | 1202 |
1 files changed, 1202 insertions, 0 deletions
diff --git a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js new file mode 100644 index 0000000000..ee2204f793 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js @@ -0,0 +1,1202 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +const { FxAccountsClient } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsClient.sys.mjs" +); +const { FxAccountsDevice } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccountsDevice.sys.mjs" +); +const { + ERRNO_DEVICE_SESSION_CONFLICT, + ERRNO_TOO_MANY_CLIENT_REQUESTS, + ERRNO_UNKNOWN_DEVICE, + ON_DEVICE_CONNECTED_NOTIFICATION, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + ON_DEVICELIST_UPDATED, +} = ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js"); +var { AccountState } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); + +initTestLogging("Trace"); + +var log = Log.repository.getLogger("Services.FxAccounts.test"); +log.level = Log.Level.Debug; + +const BOGUS_PUBLICKEY = + "BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc"; +const BOGUS_AUTHKEY = "GSsIiaD2Mr83iPqwFNK4rw"; + +Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace"); + +const DEVICE_REGISTRATION_VERSION = 42; + +function MockStorageManager() {} + +MockStorageManager.prototype = { + initialize(accountData) { + this.accountData = accountData; + }, + + finalize() { + return Promise.resolve(); + }, + + getAccountData() { + return Promise.resolve(this.accountData); + }, + + updateAccountData(updatedFields) { + for (let [name, value] of Object.entries(updatedFields)) { + if (value == null) { + delete this.accountData[name]; + } else { + this.accountData[name] = value; + } + } + return Promise.resolve(); + }, + + deleteAccountData() { + this.accountData = null; + return Promise.resolve(); + }, +}; + +function MockFxAccountsClient(device) { + this._email = "nobody@example.com"; + // Be careful relying on `this._verified` as it doesn't change if the user's + // state does via setting the `verified` flag in the user data. + this._verified = false; + this._deletedOnServer = false; // for testing accountStatus + + // mock calls up to the auth server to determine whether the + // user account has been verified + this.recoveryEmailStatus = function (sessionToken) { + // simulate a call to /recovery_email/status + return Promise.resolve({ + email: this._email, + verified: this._verified, + }); + }; + + this.accountKeys = function (keyFetchToken) { + Assert.ok(keyFetchToken, "must be called with a key-fetch-token"); + // ideally we'd check the verification status here to more closely simulate + // the server, but `this._verified` is a test-only construct and doesn't + // update when the user changes verification status. + Assert.ok(!this._deletedOnServer, "this test thinks the acct is deleted!"); + return { + kA: "test-ka", + wrapKB: "X".repeat(32), + }; + }; + + this.accountStatus = function (uid) { + return Promise.resolve(!!uid && !this._deletedOnServer); + }; + + this.registerDevice = (st, name, type) => + Promise.resolve({ id: device.id, name }); + this.updateDevice = (st, id, name) => Promise.resolve({ id, name }); + this.signOut = () => Promise.resolve({}); + this.getDeviceList = st => + Promise.resolve([ + { + id: device.id, + name: device.name, + type: device.type, + pushCallback: device.pushCallback, + pushEndpointExpired: device.pushEndpointExpired, + isCurrentDevice: st === device.sessionToken, + }, + ]); + + FxAccountsClient.apply(this); +} +MockFxAccountsClient.prototype = {}; +Object.setPrototypeOf( + MockFxAccountsClient.prototype, + FxAccountsClient.prototype +); + +async function MockFxAccounts(credentials, device = {}) { + let fxa = new FxAccounts({ + newAccountState(creds) { + // we use a real accountState but mocked storage. + let storage = new MockStorageManager(); + storage.initialize(creds); + return new AccountState(storage); + }, + fxAccountsClient: new MockFxAccountsClient(device, credentials), + fxaPushService: { + registerPushEndpoint() { + return new Promise(resolve => { + resolve({ + endpoint: "http://mochi.test:8888", + getKey(type) { + return ChromeUtils.base64URLDecode( + type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, + { padding: "ignore" } + ); + }, + }); + }); + }, + unsubscribe() { + return Promise.resolve(); + }, + }, + commands: { + async availableCommands() { + return {}; + }, + }, + device: { + DEVICE_REGISTRATION_VERSION, + _checkRemoteCommandsUpdateNeeded: async () => false, + }, + VERIFICATION_POLL_TIMEOUT_INITIAL: 1, + }); + fxa._internal.device._fxai = fxa._internal; + await fxa._internal.setSignedInUser(credentials); + Services.prefs.setStringPref( + "identity.fxaccounts.account.device.name", + device.name || "mock device name" + ); + return fxa; +} + +function updateUserAccountData(fxa, data) { + return fxa._internal.updateUserAccountData(data); +} + +add_task(async function test_updateDeviceRegistration_with_new_device() { + const deviceName = "foo"; + const deviceType = "bar"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + // Remove the current device registration (setSignedInUser does one!). + await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.resolve({ + id: "newly-generated device id", + createdAt: Date.now(), + name: deviceName, + type: deviceType, + }); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([]); + }; + + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.updateDevice.count, 0); + Assert.equal(spy.getDeviceList.count, 0); + Assert.equal(spy.registerDevice.count, 1); + Assert.equal(spy.registerDevice.args[0].length, 4); + Assert.equal(spy.registerDevice.args[0][0], credentials.sessionToken); + Assert.equal(spy.registerDevice.args[0][1], deviceName); + Assert.equal(spy.registerDevice.args[0][2], "desktop"); + Assert.equal( + spy.registerDevice.args[0][3].pushCallback, + "http://mochi.test:8888" + ); + Assert.equal(spy.registerDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); + Assert.equal(spy.registerDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(data.device.id, "newly-generated device id"); + Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION); + await fxa.signOut(true); +}); + +add_task(async function test_updateDeviceRegistration_with_existing_device() { + const deviceId = "my device id"; + const deviceName = "phil's device"; + + const credentials = getTestUser("pb"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: deviceId, + registeredCommandsKeys: [], + registrationVersion: 1, // < 42 + }, + }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({ + id: deviceId, + name: deviceName, + }); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([]); + }; + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.registerDevice.count, 0); + Assert.equal(spy.getDeviceList.count, 0); + Assert.equal(spy.updateDevice.count, 1); + Assert.equal(spy.updateDevice.args[0].length, 4); + Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); + Assert.equal(spy.updateDevice.args[0][1], deviceId); + Assert.equal(spy.updateDevice.args[0][2], deviceName); + Assert.equal( + spy.updateDevice.args[0][3].pushCallback, + "http://mochi.test:8888" + ); + Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); + Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(data.device.id, deviceId); + Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION); + await fxa.signOut(true); +}); + +add_task( + async function test_updateDeviceRegistration_with_unknown_device_error() { + const deviceName = "foo"; + const deviceType = "bar"; + const currentDeviceId = "my device id"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: currentDeviceId, + registeredCommandsKeys: [], + registrationVersion: 1, // < 42 + }, + }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.resolve({ + id: "a different newly-generated device id", + createdAt: Date.now(), + name: deviceName, + type: deviceType, + }); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.reject({ + code: 400, + errno: ERRNO_UNKNOWN_DEVICE, + }); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([]); + }; + + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.getDeviceList.count, 0); + Assert.equal(spy.registerDevice.count, 0); + Assert.equal(spy.updateDevice.count, 1); + Assert.equal(spy.updateDevice.args[0].length, 4); + Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); + Assert.equal(spy.updateDevice.args[0][1], currentDeviceId); + Assert.equal(spy.updateDevice.args[0][2], deviceName); + Assert.equal( + spy.updateDevice.args[0][3].pushCallback, + "http://mochi.test:8888" + ); + Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); + Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(null, data.device); + await fxa.signOut(true); + } +); + +add_task( + async function test_updateDeviceRegistration_with_device_session_conflict_error() { + const deviceName = "foo"; + const deviceType = "bar"; + const currentDeviceId = "my device id"; + const conflictingDeviceId = "conflicting device id"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: currentDeviceId, + registeredCommandsKeys: [], + registrationVersion: 1, // < 42 + }, + }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [], times: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + spy.updateDevice.time = Date.now(); + if (spy.updateDevice.count === 1) { + return Promise.reject({ + code: 400, + errno: ERRNO_DEVICE_SESSION_CONFLICT, + }); + } + return Promise.resolve({ + id: conflictingDeviceId, + name: deviceName, + }); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + spy.getDeviceList.time = Date.now(); + return Promise.resolve([ + { + id: "ignore", + name: "ignore", + type: "ignore", + isCurrentDevice: false, + }, + { + id: conflictingDeviceId, + name: deviceName, + type: deviceType, + isCurrentDevice: true, + }, + ]); + }; + + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.registerDevice.count, 0); + Assert.equal(spy.updateDevice.count, 1); + Assert.equal(spy.updateDevice.args[0].length, 4); + Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken); + Assert.equal(spy.updateDevice.args[0][1], currentDeviceId); + Assert.equal(spy.updateDevice.args[0][2], deviceName); + Assert.equal( + spy.updateDevice.args[0][3].pushCallback, + "http://mochi.test:8888" + ); + Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY); + Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY); + Assert.equal(spy.getDeviceList.count, 1); + Assert.equal(spy.getDeviceList.args[0].length, 1); + Assert.equal(spy.getDeviceList.args[0][0], credentials.sessionToken); + Assert.ok(spy.getDeviceList.time >= spy.updateDevice.time); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(data.device.id, conflictingDeviceId); + Assert.equal(data.device.registrationVersion, null); + await fxa.signOut(true); + } +); + +add_task( + async function test_updateDeviceRegistration_with_unrecoverable_error() { + const deviceName = "foo"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { name: deviceName }); + await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); + + const spy = { + registerDevice: { count: 0, args: [] }, + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.registerDevice = function () { + spy.registerDevice.count += 1; + spy.registerDevice.args.push(arguments); + return Promise.reject({ + code: 400, + errno: ERRNO_TOO_MANY_CLIENT_REQUESTS, + }); + }; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([]); + }; + + await fxa.updateDeviceRegistration(); + + Assert.equal(spy.getDeviceList.count, 0); + Assert.equal(spy.updateDevice.count, 0); + Assert.equal(spy.registerDevice.count, 1); + Assert.equal(spy.registerDevice.args[0].length, 4); + + const state = fxa._internal.currentAccountState; + const data = await state.getUserAccountData(); + + Assert.equal(null, data.device); + await fxa.signOut(true); + } +); + +add_task( + async function test_getDeviceId_with_no_device_id_invokes_device_registration() { + const credentials = getTestUser("foo"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + await updateUserAccountData(fxa, { uid: credentials.uid, device: null }); + + const spy = { count: 0, args: [] }; + fxa._internal.currentAccountState.getUserAccountData = () => + Promise.resolve({ + email: credentials.email, + registrationVersion: DEVICE_REGISTRATION_VERSION, + }); + fxa._internal.device._registerOrUpdateDevice = function () { + spy.count += 1; + spy.args.push(arguments); + return Promise.resolve("bar"); + }; + + const result = await fxa.device.getLocalId(); + + Assert.equal(spy.count, 1); + Assert.equal(spy.args[0].length, 2); + Assert.equal(spy.args[0][1].email, credentials.email); + Assert.equal(null, spy.args[0][1].device); + Assert.equal(result, "bar"); + await fxa.signOut(true); + } +); + +add_task( + async function test_getDeviceId_with_registration_version_outdated_invokes_device_registration() { + const credentials = getTestUser("foo"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + + const spy = { count: 0, args: [] }; + fxa._internal.currentAccountState.getUserAccountData = () => + Promise.resolve({ + device: { + id: "my id", + registrationVersion: 0, + registeredCommandsKeys: [], + }, + }); + fxa._internal.device._registerOrUpdateDevice = function () { + spy.count += 1; + spy.args.push(arguments); + return Promise.resolve("wibble"); + }; + + const result = await fxa.device.getLocalId(); + + Assert.equal(spy.count, 1); + Assert.equal(spy.args[0].length, 2); + Assert.equal(spy.args[0][1].device.id, "my id"); + Assert.equal(result, "wibble"); + await fxa.signOut(true); + } +); + +add_task( + async function test_getDeviceId_with_device_id_and_uptodate_registration_version_doesnt_invoke_device_registration() { + const credentials = getTestUser("foo"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + + const spy = { count: 0 }; + fxa._internal.currentAccountState.getUserAccountData = async () => ({ + device: { + id: "foo's device id", + registrationVersion: DEVICE_REGISTRATION_VERSION, + registeredCommandsKeys: [], + }, + }); + fxa._internal.device._registerOrUpdateDevice = function () { + spy.count += 1; + return Promise.resolve("bar"); + }; + + const result = await fxa.device.getLocalId(); + + Assert.equal(spy.count, 0); + Assert.equal(result, "foo's device id"); + await fxa.signOut(true); + } +); + +add_task( + async function test_getDeviceId_with_device_id_and_with_no_registration_version_invokes_device_registration() { + const credentials = getTestUser("foo"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + + const spy = { count: 0, args: [] }; + fxa._internal.currentAccountState.getUserAccountData = () => + Promise.resolve({ device: { id: "wibble" } }); + fxa._internal.device._registerOrUpdateDevice = function () { + spy.count += 1; + spy.args.push(arguments); + return Promise.resolve("wibble"); + }; + + const result = await fxa.device.getLocalId(); + + Assert.equal(spy.count, 1); + Assert.equal(spy.args[0].length, 2); + Assert.equal(spy.args[0][1].device.id, "wibble"); + Assert.equal(result, "wibble"); + await fxa.signOut(true); + } +); + +add_task(async function test_verification_updates_registration() { + const deviceName = "foo"; + + const credentials = getTestUser("baz"); + const fxa = await MockFxAccounts(credentials, { + id: "device-id", + name: deviceName, + }); + + // We should already have a device registration, but without send-tab due to + // our inability to fetch keys for an unverified users. + const state = fxa._internal.currentAccountState; + const { device } = await state.getUserAccountData(); + Assert.equal(device.registeredCommandsKeys.length, 0); + + let updatePromise = new Promise(resolve => { + const old_registerOrUpdateDevice = fxa.device._registerOrUpdateDevice.bind( + fxa.device + ); + fxa.device._registerOrUpdateDevice = async function ( + currentState, + signedInUser + ) { + await old_registerOrUpdateDevice(currentState, signedInUser); + fxa.device._registerOrUpdateDevice = old_registerOrUpdateDevice; + resolve(); + }; + }); + + fxa._internal.checkEmailStatus = async function (sessionToken) { + credentials.verified = true; + return credentials; + }; + + await updatePromise; + + const { device: newDevice, encryptedSendTabKeys } = + await state.getUserAccountData(); + Assert.equal(newDevice.registeredCommandsKeys.length, 1); + Assert.notEqual(encryptedSendTabKeys, null); + await fxa.signOut(true); +}); + +add_task(async function test_devicelist_pushendpointexpired() { + const deviceId = "mydeviceid"; + const credentials = getTestUser("baz"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: deviceId, + registeredCommandsKeys: [], + registrationVersion: 1, // < 42 + }, + }); + + const spy = { + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([ + { + id: "mydeviceid", + name: "foo", + type: "desktop", + isCurrentDevice: true, + pushEndpointExpired: true, + pushCallback: "https://example.com", + }, + ]); + }; + let polledForMissedCommands = false; + fxa._internal.commands.pollDeviceCommands = () => { + polledForMissedCommands = true; + }; + + await fxa.device.refreshDeviceList(); + + Assert.equal(spy.getDeviceList.count, 1); + Assert.equal(spy.updateDevice.count, 1); + Assert.ok(polledForMissedCommands); + await fxa.signOut(true); +}); + +add_task(async function test_devicelist_nopushcallback() { + const deviceId = "mydeviceid"; + const credentials = getTestUser("baz"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + await updateUserAccountData(fxa, { + uid: credentials.uid, + device: { + id: deviceId, + registeredCommandsKeys: [], + registrationVersion: 1, + }, + }); + + const spy = { + updateDevice: { count: 0, args: [] }, + getDeviceList: { count: 0, args: [] }, + }; + const client = fxa._internal.fxAccountsClient; + client.updateDevice = function () { + spy.updateDevice.count += 1; + spy.updateDevice.args.push(arguments); + return Promise.resolve({}); + }; + client.getDeviceList = function () { + spy.getDeviceList.count += 1; + spy.getDeviceList.args.push(arguments); + return Promise.resolve([ + { + id: "mydeviceid", + name: "foo", + type: "desktop", + isCurrentDevice: true, + pushEndpointExpired: false, + pushCallback: null, + }, + ]); + }; + + let polledForMissedCommands = false; + fxa._internal.commands.pollDeviceCommands = () => { + polledForMissedCommands = true; + }; + + await fxa.device.refreshDeviceList(); + + Assert.equal(spy.getDeviceList.count, 1); + Assert.equal(spy.updateDevice.count, 1); + Assert.ok(polledForMissedCommands); + await fxa.signOut(true); +}); + +add_task(async function test_refreshDeviceList() { + let credentials = getTestUser("baz"); + + let storage = new MockStorageManager(); + storage.initialize(credentials); + let state = new AccountState(storage); + + let fxAccountsClient = new MockFxAccountsClient({ + id: "deviceAAAAAA", + name: "iPhone", + type: "phone", + pushCallback: "http://mochi.test:8888", + pushEndpointExpired: false, + sessionToken: credentials.sessionToken, + }); + let spy = { + getDeviceList: { count: 0 }, + }; + const deviceListUpdateObserver = { + count: 0, + observe(subject, topic, data) { + this.count++; + }, + }; + Services.obs.addObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED); + + fxAccountsClient.getDeviceList = (function (old) { + return function getDeviceList() { + spy.getDeviceList.count += 1; + return old.apply(this, arguments); + }; + })(fxAccountsClient.getDeviceList); + let fxai = { + _now: Date.now(), + _generation: 0, + fxAccountsClient, + now() { + return this._now; + }, + withVerifiedAccountState(func) { + // Ensure `func` is called asynchronously, and simulate the possibility + // of a different user signng in while the promise is in-flight. + const currentGeneration = this._generation; + return Promise.resolve() + .then(_ => func(state)) + .then(result => { + if (currentGeneration < this._generation) { + throw new Error("Another user has signed in"); + } + return result; + }); + }, + fxaPushService: { + registerPushEndpoint() { + return new Promise(resolve => { + resolve({ + endpoint: "http://mochi.test:8888", + getKey(type) { + return ChromeUtils.base64URLDecode( + type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, + { padding: "ignore" } + ); + }, + }); + }); + }, + unsubscribe() { + return Promise.resolve(); + }, + getSubscription() { + return Promise.resolve({ + isExpired: () => { + return false; + }, + endpoint: "http://mochi.test:8888", + }); + }, + }, + async _handleTokenError(e) { + _(`Test failure: ${e} - ${e.stack}`); + throw e; + }, + }; + let device = new FxAccountsDevice(fxai); + device._checkRemoteCommandsUpdateNeeded = async () => false; + + Assert.equal( + device.recentDeviceList, + null, + "Should not have device list initially" + ); + Assert.ok(await device.refreshDeviceList(), "Should refresh list"); + Assert.equal( + deviceListUpdateObserver.count, + 1, + `${ON_DEVICELIST_UPDATED} was notified` + ); + Assert.deepEqual( + device.recentDeviceList, + [ + { + id: "deviceAAAAAA", + name: "iPhone", + type: "phone", + pushCallback: "http://mochi.test:8888", + pushEndpointExpired: false, + isCurrentDevice: true, + }, + ], + "Should fetch device list" + ); + Assert.equal( + spy.getDeviceList.count, + 1, + "Should make request to refresh list" + ); + Assert.ok( + !(await device.refreshDeviceList()), + "Should not refresh device list if fresh" + ); + Assert.equal( + deviceListUpdateObserver.count, + 1, + `${ON_DEVICELIST_UPDATED} was not notified` + ); + + fxai._now += device.TIME_BETWEEN_FXA_DEVICES_FETCH_MS; + + let refreshPromise = device.refreshDeviceList(); + let secondRefreshPromise = device.refreshDeviceList(); + Assert.ok( + await Promise.all([refreshPromise, secondRefreshPromise]), + "Should refresh list if stale" + ); + Assert.equal( + spy.getDeviceList.count, + 2, + "Should only make one request if called with pending request" + ); + Assert.equal( + deviceListUpdateObserver.count, + 2, + `${ON_DEVICELIST_UPDATED} only notified once` + ); + + device.observe(null, ON_DEVICE_CONNECTED_NOTIFICATION); + await device.refreshDeviceList(); + Assert.equal( + spy.getDeviceList.count, + 3, + "Should refresh device list after connecting new device" + ); + Assert.equal( + deviceListUpdateObserver.count, + 3, + `${ON_DEVICELIST_UPDATED} notified when new device connects` + ); + device.observe( + null, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + JSON.stringify({ isLocalDevice: false }) + ); + await device.refreshDeviceList(); + Assert.equal( + spy.getDeviceList.count, + 4, + "Should refresh device list after disconnecting device" + ); + Assert.equal( + deviceListUpdateObserver.count, + 4, + `${ON_DEVICELIST_UPDATED} notified when device disconnects` + ); + device.observe( + null, + ON_DEVICE_DISCONNECTED_NOTIFICATION, + JSON.stringify({ isLocalDevice: true }) + ); + await device.refreshDeviceList(); + Assert.equal( + spy.getDeviceList.count, + 4, + "Should not refresh device list after disconnecting this device" + ); + Assert.equal( + deviceListUpdateObserver.count, + 4, + `${ON_DEVICELIST_UPDATED} not notified again` + ); + + let refreshBeforeResetPromise = device.refreshDeviceList({ + ignoreCached: true, + }); + fxai._generation++; + Assert.equal( + deviceListUpdateObserver.count, + 4, + `${ON_DEVICELIST_UPDATED} not notified` + ); + await Assert.rejects(refreshBeforeResetPromise, /Another user has signed in/); + + device.reset(); + Assert.equal( + device.recentDeviceList, + null, + "Should clear device list after resetting" + ); + Assert.ok( + await device.refreshDeviceList(), + "Should fetch new list after resetting" + ); + Assert.equal( + deviceListUpdateObserver.count, + 5, + `${ON_DEVICELIST_UPDATED} notified after reset` + ); + Services.obs.removeObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED); +}); + +add_task(async function test_push_resubscribe() { + let credentials = getTestUser("baz"); + + let storage = new MockStorageManager(); + storage.initialize(credentials); + let state = new AccountState(storage); + + let mockDevice = { + id: "deviceAAAAAA", + name: "iPhone", + type: "phone", + pushCallback: "http://mochi.test:8888", + pushEndpointExpired: false, + sessionToken: credentials.sessionToken, + }; + + var mockSubscription = { + isExpired: () => { + return false; + }, + endpoint: "http://mochi.test:8888", + }; + + let fxAccountsClient = new MockFxAccountsClient(mockDevice); + + const spy = { + _registerOrUpdateDevice: { count: 0 }, + }; + + let fxai = { + _now: Date.now(), + _generation: 0, + fxAccountsClient, + now() { + return this._now; + }, + withVerifiedAccountState(func) { + // Ensure `func` is called asynchronously, and simulate the possibility + // of a different user signng in while the promise is in-flight. + const currentGeneration = this._generation; + return Promise.resolve() + .then(_ => func(state)) + .then(result => { + if (currentGeneration < this._generation) { + throw new Error("Another user has signed in"); + } + return result; + }); + }, + fxaPushService: { + registerPushEndpoint() { + return new Promise(resolve => { + resolve({ + endpoint: "http://mochi.test:8888", + getKey(type) { + return ChromeUtils.base64URLDecode( + type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY, + { padding: "ignore" } + ); + }, + }); + }); + }, + unsubscribe() { + return Promise.resolve(); + }, + getSubscription() { + return Promise.resolve(mockSubscription); + }, + }, + commands: { + async pollDeviceCommands() {}, + }, + async _handleTokenError(e) { + _(`Test failure: ${e} - ${e.stack}`); + throw e; + }, + }; + let device = new FxAccountsDevice(fxai); + device._checkRemoteCommandsUpdateNeeded = async () => false; + device._registerOrUpdateDevice = async () => { + spy._registerOrUpdateDevice.count += 1; + }; + + Assert.ok(await device.refreshDeviceList(), "Should refresh list"); + Assert.equal(spy._registerOrUpdateDevice.count, 0, "not expecting a refresh"); + + mockDevice.pushEndpointExpired = true; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 1, + "end-point expired means should resubscribe" + ); + + mockDevice.pushEndpointExpired = false; + mockSubscription.isExpired = () => true; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 2, + "push service saying expired should resubscribe" + ); + + mockSubscription.isExpired = () => false; + mockSubscription.endpoint = "something-else"; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 3, + "push service endpoint diff should resubscribe" + ); + + mockSubscription = null; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 4, + "push service saying no sub should resubscribe" + ); + + // reset everything to make sure we didn't leave something behind causing the above to + // not check what we thought it was. + mockSubscription = { + isExpired: () => { + return false; + }, + endpoint: "http://mochi.test:8888", + }; + Assert.ok( + await device.refreshDeviceList({ ignoreCached: true }), + "Should refresh list" + ); + Assert.equal( + spy._registerOrUpdateDevice.count, + 4, + "resetting to good data should not resubscribe" + ); +}); + +add_task(async function test_checking_remote_availableCommands_mismatch() { + const credentials = getTestUser("baz"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + fxa.device._checkRemoteCommandsUpdateNeeded = + FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded; + fxa.commands.availableCommands = async () => { + return { + "https://identity.mozilla.com/cmd/open-uri": "local-keys", + }; + }; + + const ourDevice = { + isCurrentDevice: true, + availableCommands: { + "https://identity.mozilla.com/cmd/open-uri": "remote-keys", + }, + }; + Assert.ok( + await fxa.device._checkRemoteCommandsUpdateNeeded( + ourDevice.availableCommands + ) + ); +}); + +add_task(async function test_checking_remote_availableCommands_match() { + const credentials = getTestUser("baz"); + credentials.verified = true; + const fxa = await MockFxAccounts(credentials); + fxa.device._checkRemoteCommandsUpdateNeeded = + FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded; + fxa.commands.availableCommands = async () => { + return { + "https://identity.mozilla.com/cmd/open-uri": "local-keys", + }; + }; + + const ourDevice = { + isCurrentDevice: true, + availableCommands: { + "https://identity.mozilla.com/cmd/open-uri": "local-keys", + }, + }; + Assert.ok( + !(await fxa.device._checkRemoteCommandsUpdateNeeded( + ourDevice.availableCommands + )) + ); +}); + +function getTestUser(name) { + return { + email: name + "@example.com", + uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348", + sessionToken: name + "'s session token", + verified: false, + ...MOCK_ACCOUNT_KEYS, + }; +} |