/* 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"
);
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({
endpoint:
"http://mochi.test:8888",
});
});
},
},
});
}
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");
});