255 lines
7.1 KiB
JavaScript
255 lines
7.1 KiB
JavaScript
/* 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");
|
|
});
|