Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: os == 'android' OR appname == 'thunderbird' && !nightly_build
- Manifest: services/fxaccounts/tests/xpcshell/xpcshell.toml
/* Any copyright is dedicated to the Public Domain.
"use strict";
const { FxAccounts } = ChromeUtils.importESModule(
"resource://gre/modules/FxAccounts.sys.mjs"
);
const { FxAccountsClient } = ChromeUtils.importESModule(
"resource://gre/modules/FxAccountsClient.sys.mjs"
);
var { AccountState } = ChromeUtils.importESModule(
"resource://gre/modules/FxAccounts.sys.mjs"
);
function promiseNotification(topic) {
return new Promise(resolve => {
let observe = () => {
Services.obs.removeObserver(observe, topic);
resolve();
};
Services.obs.addObserver(observe, topic);
});
}
// Just enough mocks so we can avoid hawk and storage.
function MockStorageManager() {}
MockStorageManager.prototype = {
promiseInitialized: Promise.resolve(),
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(activeTokens) {
this._email = "nobody@example.com";
this._verified = false;
this.accountStatus = function (uid) {
return Promise.resolve(!!uid && !this._deletedOnServer);
};
this.signOut = function () {
return Promise.resolve();
};
this.registerDevice = function () {
return Promise.resolve();
};
this.updateDevice = function () {
return Promise.resolve();
};
this.signOutAndDestroyDevice = function () {
return Promise.resolve();
};
this.getDeviceList = function () {
return Promise.resolve();
};
this.accessTokenWithSessionToken = function (
sessionTokenHex,
clientId,
scope,
ttl
) {
let token = `token${this.numTokenFetches}`;
if (ttl) {
token += `-ttl-${ttl}`;
}
this.numTokenFetches += 1;
this.activeTokens.add(token);
print("accessTokenWithSessionToken returning token", token);
return Promise.resolve({ access_token: token, ttl });
};
this.oauthDestroy = sinon.stub().callsFake((_clientId, token) => {
this.activeTokens.delete(token);
return Promise.resolve();
});
// Test only stuff.
this.activeTokens = activeTokens;
this.numTokenFetches = 0;
FxAccountsClient.apply(this);
}
MockFxAccountsClient.prototype = {};
Object.setPrototypeOf(
MockFxAccountsClient.prototype,
FxAccountsClient.prototype
);
function MockFxAccounts() {
// The FxA "auth" and "oauth" servers both share the same db of tokens,
// so we need to simulate the same here in the tests.
const activeTokens = new Set();
return new FxAccounts({
fxAccountsClient: new MockFxAccountsClient(activeTokens),
newAccountState(credentials) {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(storage);
},
_getDeviceName() {
return "mock device name";
},
fxaPushService: {
registerPushEndpoint() {
return new Promise(resolve => {
resolve({
});
});
},
},
});
}
async function createMockFxA() {
let fxa = new MockFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
sessionToken: "dead",
scopedKeys: {
[SCOPE_OLD_SYNC]: {
kid: "key id for sync key",
k: "key material for sync key",
kty: "oct",
},
},
verified: true,
};
await fxa._internal.setSignedInUser(credentials);
return fxa;
}
// The tests.
add_task(async function testRevoke() {
let tokenOptions = { scope: "test-scope" };
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
// get our first token and check we hit the mock.
let token1 = await fxa.getOAuthToken(tokenOptions);
equal(client.numTokenFetches, 1);
equal(client.activeTokens.size, 1);
ok(token1, "got a token");
equal(token1, "token0");
// drop the new token from our cache.
await fxa.removeCachedOAuthToken({ token: token1 });
ok(client.oauthDestroy.calledOnce);
// the revoke should have been successful.
equal(client.activeTokens.size, 0);
// fetching it again hits the server.
let token2 = await fxa.getOAuthToken(tokenOptions);
equal(client.numTokenFetches, 2);
equal(client.activeTokens.size, 1);
ok(token2, "got a token");
notEqual(token1, token2, "got a different token");
});
add_task(async function testSignOutDestroysTokens() {
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
// get our first token and check we hit the mock.
let token1 = await fxa.getOAuthToken({ scope: "test-scope" });
equal(client.numTokenFetches, 1);
equal(client.activeTokens.size, 1);
ok(token1, "got a token");
// get another
let token2 = await fxa.getOAuthToken({ scope: "test-scope-2" });
equal(client.numTokenFetches, 2);
equal(client.activeTokens.size, 2);
ok(token2, "got a token");
notEqual(token1, token2, "got a different token");
// FxA fires an observer when the "background" signout is complete.
let signoutComplete = promiseNotification("testhelper-fxa-signout-complete");
// now sign out - they should be removed.
await fxa.signOut();
await signoutComplete;
ok(client.oauthDestroy.calledTwice);
// No active tokens left.
equal(client.activeTokens.size, 0);
});
add_task(async function testTokenRaces() {
// Here we do 2 concurrent fetches each for 2 different token scopes (ie,
// 4 token fetches in total).
// This should provoke a potential race in the token fetching but we use
// a map of in-flight token fetches, so we should still only perform 2
// fetches, but each of the 4 calls should resolve with the correct values.
let fxa = await createMockFxA();
let client = fxa._internal.fxAccountsClient;
let results = await Promise.all([
fxa.getOAuthToken({ scope: "test-scope" }),
fxa.getOAuthToken({ scope: "test-scope" }),
fxa.getOAuthToken({ scope: "test-scope-2" }),
fxa.getOAuthToken({ scope: "test-scope-2" }),
]);
equal(client.numTokenFetches, 2, "should have fetched 2 tokens.");
// Should have 2 unique tokens
results.sort();
equal(results[0], results[1]);
equal(results[2], results[3]);
// should be 2 active.
equal(client.activeTokens.size, 2);
await fxa.removeCachedOAuthToken({ token: results[0] });
equal(client.activeTokens.size, 1);
await fxa.removeCachedOAuthToken({ token: results[2] });
equal(client.activeTokens.size, 0);
ok(client.oauthDestroy.calledTwice);
});
add_task(async function testTokenTTL() {
// This tests the TTL option passed into the method
let fxa = await createMockFxA();
let token = await fxa.getOAuthToken({ scope: "test-ttl", ttl: 1000 });
equal(token, "token0-ttl-1000");
});