diff options
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_oauth_tokens.js')
-rw-r--r-- | services/fxaccounts/tests/xpcshell/test_oauth_tokens.js | 251 |
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; +}); |