Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
"use strict";
const { FxAccounts } = ChromeUtils.importESModule(
);
const { FxAccountsClient } = ChromeUtils.importESModule(
);
const { FxAccountsDevice } = ChromeUtils.importESModule(
);
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.importESModule(
);
var { AccountState } = ChromeUtils.importESModule(
);
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.setStringPref("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 () {
// 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) => 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,
);
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,
);
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,
);
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,
);
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 () {
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() {
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 {
};
};
const ourDevice = {
isCurrentDevice: true,
availableCommands: {
},
};
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 {
};
};
const ourDevice = {
isCurrentDevice: true,
availableCommands: {
},
};
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,
};
}