summaryrefslogtreecommitdiff
path: root/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_oauth_tokens.js')
-rw-r--r--services/fxaccounts/tests/xpcshell/test_oauth_tokens.js251
1 files changed, 251 insertions, 0 deletions
diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
new file mode 100644
index 0000000000..f758bf4050
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
+Cu.import("resource://services-common/utils.js");
+var {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {});
+
+function promiseNotification(topic) {
+ return new Promise(resolve => {
+ let observe = () => {
+ Services.obs.removeObserver(observe, topic);
+ resolve();
+ }
+ Services.obs.addObserver(observe, topic, false);
+ });
+}
+
+// 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() {
+ this._email = "nobody@example.com";
+ this._verified = false;
+
+ this.accountStatus = function(uid) {
+ let deferred = Promise.defer();
+ deferred.resolve(!!uid && (!this._deletedOnServer));
+ return deferred.promise;
+ };
+
+ 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(); };
+
+ FxAccountsClient.apply(this);
+}
+
+MockFxAccountsClient.prototype = {
+ __proto__: FxAccountsClient.prototype
+}
+
+function MockFxAccounts(mockGrantClient) {
+ return new FxAccounts({
+ fxAccountsClient: new MockFxAccountsClient(),
+ getAssertion: () => Promise.resolve("assertion"),
+ newAccountState(credentials) {
+ // we use a real accountState but mocked storage.
+ let storage = new MockStorageManager();
+ storage.initialize(credentials);
+ return new AccountState(storage);
+ },
+ _destroyOAuthToken: function(tokenData) {
+ // somewhat sad duplication of _destroyOAuthToken, but hard to avoid.
+ return mockGrantClient.destroyToken(tokenData.token).then( () => {
+ Services.obs.notifyObservers(null, "testhelper-fxa-revoke-complete", null);
+ });
+ },
+ _getDeviceName() {
+ return "mock device name";
+ },
+ fxaPushService: {
+ registerPushEndpoint() {
+ return new Promise((resolve) => {
+ resolve({
+ endpoint: "http://mochi.test:8888"
+ });
+ });
+ },
+ },
+ });
+}
+
+function* createMockFxA(mockGrantClient) {
+ let fxa = new MockFxAccounts(mockGrantClient);
+ let credentials = {
+ email: "foo@example.com",
+ uid: "1234@lcip.org",
+ assertion: "foobar",
+ sessionToken: "dead",
+ kA: "beef",
+ kB: "cafe",
+ verified: true
+ };
+
+ yield fxa.setSignedInUser(credentials);
+ return fxa;
+}
+
+// The tests.
+function run_test() {
+ run_next_test();
+}
+
+function MockFxAccountsOAuthGrantClient() {
+ this.activeTokens = new Set();
+}
+
+MockFxAccountsOAuthGrantClient.prototype = {
+ serverURL: {href: "http://localhost"},
+ getTokenFromAssertion(assertion, scope) {
+ let token = "token" + this.numTokenFetches;
+ this.numTokenFetches += 1;
+ this.activeTokens.add(token);
+ print("getTokenFromAssertion returning token", token);
+ return Promise.resolve({access_token: token});
+ },
+ destroyToken(token) {
+ ok(this.activeTokens.delete(token));
+ print("after destroy have", this.activeTokens.size, "tokens left.");
+ return Promise.resolve({});
+ },
+ // and some stuff used only for tests.
+ numTokenFetches: 0,
+ activeTokens: null,
+}
+
+add_task(function* testRevoke() {
+ let client = new MockFxAccountsOAuthGrantClient();
+ let tokenOptions = { scope: "test-scope", client: client };
+ let fxa = yield createMockFxA(client);
+
+ // get our first token and check we hit the mock.
+ let token1 = yield 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.
+ yield fxa.removeCachedOAuthToken({token: token1});
+
+ // FxA fires an observer when the "background" revoke is complete.
+ yield promiseNotification("testhelper-fxa-revoke-complete");
+ // the revoke should have been successful.
+ equal(client.activeTokens.size, 0);
+ // fetching it again hits the server.
+ let token2 = yield 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(function* testSignOutDestroysTokens() {
+ let client = new MockFxAccountsOAuthGrantClient();
+ let fxa = yield createMockFxA(client);
+
+ // get our first token and check we hit the mock.
+ let token1 = yield fxa.getOAuthToken({ scope: "test-scope", client: client });
+ equal(client.numTokenFetches, 1);
+ equal(client.activeTokens.size, 1);
+ ok(token1, "got a token");
+
+ // get another
+ let token2 = yield fxa.getOAuthToken({ scope: "test-scope-2", client: client });
+ equal(client.numTokenFetches, 2);
+ equal(client.activeTokens.size, 2);
+ ok(token2, "got a token");
+ notEqual(token1, token2, "got a different token");
+
+ // now sign out - they should be removed.
+ yield fxa.signOut();
+ // FxA fires an observer when the "background" signout is complete.
+ yield promiseNotification("testhelper-fxa-signout-complete");
+ // No active tokens left.
+ equal(client.activeTokens.size, 0);
+});
+
+add_task(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 should
+ // handle and detect that leaving us with one of the fetch tokens being
+ // revoked and the same token value returned to both calls.
+ let client = new MockFxAccountsOAuthGrantClient();
+ let fxa = yield createMockFxA(client);
+
+ // We should see 2 notifications as part of this - set up the listeners
+ // now (and wait on them later)
+ let notifications = Promise.all([
+ promiseNotification("testhelper-fxa-revoke-complete"),
+ promiseNotification("testhelper-fxa-revoke-complete"),
+ ]);
+ let results = yield Promise.all([
+ fxa.getOAuthToken({scope: "test-scope", client: client}),
+ fxa.getOAuthToken({scope: "test-scope", client: client}),
+ fxa.getOAuthToken({scope: "test-scope-2", client: client}),
+ fxa.getOAuthToken({scope: "test-scope-2", client: client}),
+ ]);
+
+ equal(client.numTokenFetches, 4, "should have fetched 4 tokens.");
+ // We should see 2 of the 4 revoked due to the race.
+ yield notifications;
+
+ // 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);
+ // Which can each be revoked.
+ notifications = Promise.all([
+ promiseNotification("testhelper-fxa-revoke-complete"),
+ promiseNotification("testhelper-fxa-revoke-complete"),
+ ]);
+ yield fxa.removeCachedOAuthToken({token: results[0]});
+ equal(client.activeTokens.size, 1);
+ yield fxa.removeCachedOAuthToken({token: results[2]});
+ equal(client.activeTokens.size, 0);
+ yield notifications;
+});