michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: Cu.import("resource://gre/modules/FxAccounts.jsm"); michael@0: Cu.import("resource://services-sync/browserid_identity.js"); michael@0: Cu.import("resource://services-sync/rest.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: Cu.import("resource://services-crypto/utils.js"); michael@0: Cu.import("resource://testing-common/services/sync/utils.js"); michael@0: Cu.import("resource://testing-common/services/sync/fxa_utils.js"); michael@0: Cu.import("resource://services-common/hawkclient.js"); michael@0: Cu.import("resource://gre/modules/FxAccounts.jsm"); michael@0: Cu.import("resource://gre/modules/FxAccountsClient.jsm"); michael@0: Cu.import("resource://gre/modules/FxAccountsCommon.js"); michael@0: Cu.import("resource://services-sync/service.js"); michael@0: Cu.import("resource://services-sync/status.js"); michael@0: Cu.import("resource://services-sync/constants.js"); michael@0: michael@0: const SECOND_MS = 1000; michael@0: const MINUTE_MS = SECOND_MS * 60; michael@0: const HOUR_MS = MINUTE_MS * 60; michael@0: michael@0: let identityConfig = makeIdentityConfig(); michael@0: let browseridManager = new BrowserIDManager(); michael@0: configureFxAccountIdentity(browseridManager, identityConfig); michael@0: michael@0: /** michael@0: * Mock client clock and skew vs server in FxAccounts signed-in user module and michael@0: * API client. browserid_identity.js queries these values to construct HAWK michael@0: * headers. We will use this to test clock skew compensation in these headers michael@0: * below. michael@0: */ michael@0: let MockFxAccountsClient = function() { michael@0: FxAccountsClient.apply(this); michael@0: }; michael@0: MockFxAccountsClient.prototype = { michael@0: __proto__: FxAccountsClient.prototype michael@0: }; michael@0: michael@0: function MockFxAccounts() { michael@0: let fxa = new FxAccounts({ michael@0: _now_is: Date.now(), michael@0: michael@0: now: function () { michael@0: return this._now_is; michael@0: }, michael@0: michael@0: fxAccountsClient: new MockFxAccountsClient() michael@0: }); michael@0: fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { michael@0: this.cert = { michael@0: validUntil: fxa.internal.now() + CERT_LIFETIME, michael@0: cert: "certificate", michael@0: }; michael@0: return Promise.resolve(this.cert.cert); michael@0: }; michael@0: return fxa; michael@0: } michael@0: michael@0: function run_test() { michael@0: initTestLogging("Trace"); michael@0: Log.repository.getLogger("Sync.Identity").level = Log.Level.Trace; michael@0: Log.repository.getLogger("Sync.BrowserIDManager").level = Log.Level.Trace; michael@0: run_next_test(); michael@0: }; michael@0: michael@0: add_test(function test_initial_state() { michael@0: _("Verify initial state"); michael@0: do_check_false(!!browseridManager._token); michael@0: do_check_false(browseridManager.hasValidToken()); michael@0: run_next_test(); michael@0: } michael@0: ); michael@0: michael@0: add_task(function test_initialializeWithCurrentIdentity() { michael@0: _("Verify start after initializeWithCurrentIdentity"); michael@0: browseridManager.initializeWithCurrentIdentity(); michael@0: yield browseridManager.whenReadyToAuthenticate.promise; michael@0: do_check_true(!!browseridManager._token); michael@0: do_check_true(browseridManager.hasValidToken()); michael@0: do_check_eq(browseridManager.account, identityConfig.fxaccount.user.email); michael@0: } michael@0: ); michael@0: michael@0: michael@0: add_test(function test_getResourceAuthenticator() { michael@0: _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header."); michael@0: let authenticator = browseridManager.getResourceAuthenticator(); michael@0: do_check_true(!!authenticator); michael@0: let req = {uri: CommonUtils.makeURI( michael@0: "https://example.net/somewhere/over/the/rainbow"), michael@0: method: 'GET'}; michael@0: let output = authenticator(req, 'GET'); michael@0: do_check_true('headers' in output); michael@0: do_check_true('authorization' in output.headers); michael@0: do_check_true(output.headers.authorization.startsWith('Hawk')); michael@0: _("Expected internal state after successful call."); michael@0: do_check_eq(browseridManager._token.uid, identityConfig.fxaccount.token.uid); michael@0: run_next_test(); michael@0: } michael@0: ); michael@0: michael@0: add_test(function test_getRESTRequestAuthenticator() { michael@0: _("BrowserIDManager supplies a REST Request Authenticator callback which sets a Hawk header on a request object."); michael@0: let request = new SyncStorageRequest( michael@0: "https://example.net/somewhere/over/the/rainbow"); michael@0: let authenticator = browseridManager.getRESTRequestAuthenticator(); michael@0: do_check_true(!!authenticator); michael@0: let output = authenticator(request, 'GET'); michael@0: do_check_eq(request.uri, output.uri); michael@0: do_check_true(output._headers.authorization.startsWith('Hawk')); michael@0: do_check_true(output._headers.authorization.contains('nonce')); michael@0: do_check_true(browseridManager.hasValidToken()); michael@0: run_next_test(); michael@0: } michael@0: ); michael@0: michael@0: add_test(function test_resourceAuthenticatorSkew() { michael@0: _("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header."); michael@0: michael@0: // Clock is skewed 12 hours into the future michael@0: // We pick a date in the past so we don't risk concealing bugs in code that michael@0: // uses new Date() instead of our given date. michael@0: let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; michael@0: let browseridManager = new BrowserIDManager(); michael@0: let hawkClient = new HawkClient("https://example.net/v1", "/foo"); michael@0: michael@0: // mock fxa hawk client skew michael@0: hawkClient.now = function() { michael@0: dump("mocked client now: " + now + '\n'); michael@0: return now; michael@0: } michael@0: // Imagine there's already been one fxa request and the hawk client has michael@0: // already detected skew vs the fxa auth server. michael@0: let localtimeOffsetMsec = -1 * 12 * HOUR_MS; michael@0: hawkClient._localtimeOffsetMsec = localtimeOffsetMsec; michael@0: michael@0: let fxaClient = new MockFxAccountsClient(); michael@0: fxaClient.hawk = hawkClient; michael@0: michael@0: // Sanity check michael@0: do_check_eq(hawkClient.now(), now); michael@0: do_check_eq(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec); michael@0: michael@0: // Properly picked up by the client michael@0: do_check_eq(fxaClient.now(), now); michael@0: do_check_eq(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec); michael@0: michael@0: let fxa = new MockFxAccounts(); michael@0: fxa.internal._now_is = now; michael@0: fxa.internal.fxAccountsClient = fxaClient; michael@0: michael@0: // Picked up by the signed-in user module michael@0: do_check_eq(fxa.internal.now(), now); michael@0: do_check_eq(fxa.internal.localtimeOffsetMsec, localtimeOffsetMsec); michael@0: michael@0: do_check_eq(fxa.now(), now); michael@0: do_check_eq(fxa.localtimeOffsetMsec, localtimeOffsetMsec); michael@0: michael@0: // Mocks within mocks... michael@0: configureFxAccountIdentity(browseridManager, identityConfig); michael@0: michael@0: // Ensure the new FxAccounts mock has a signed-in user. michael@0: fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; michael@0: michael@0: browseridManager._fxaService = fxa; michael@0: michael@0: do_check_eq(browseridManager._fxaService.internal.now(), now); michael@0: do_check_eq(browseridManager._fxaService.internal.localtimeOffsetMsec, michael@0: localtimeOffsetMsec); michael@0: michael@0: do_check_eq(browseridManager._fxaService.now(), now); michael@0: do_check_eq(browseridManager._fxaService.localtimeOffsetMsec, michael@0: localtimeOffsetMsec); michael@0: michael@0: let request = new SyncStorageRequest("https://example.net/i/like/pie/"); michael@0: let authenticator = browseridManager.getResourceAuthenticator(); michael@0: let output = authenticator(request, 'GET'); michael@0: dump("output" + JSON.stringify(output)); michael@0: let authHeader = output.headers.authorization; michael@0: do_check_true(authHeader.startsWith('Hawk')); michael@0: michael@0: // Skew correction is applied in the header and we're within the two-minute michael@0: // window. michael@0: do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); michael@0: do_check_true( michael@0: (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_RESTResourceAuthenticatorSkew() { michael@0: _("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header."); michael@0: michael@0: // Clock is skewed 12 hours into the future from our arbitary date michael@0: let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS; michael@0: let browseridManager = new BrowserIDManager(); michael@0: let hawkClient = new HawkClient("https://example.net/v1", "/foo"); michael@0: michael@0: // mock fxa hawk client skew michael@0: hawkClient.now = function() { michael@0: return now; michael@0: } michael@0: // Imagine there's already been one fxa request and the hawk client has michael@0: // already detected skew vs the fxa auth server. michael@0: hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS; michael@0: michael@0: let fxaClient = new MockFxAccountsClient(); michael@0: fxaClient.hawk = hawkClient; michael@0: let fxa = new MockFxAccounts(); michael@0: fxa.internal._now_is = now; michael@0: fxa.internal.fxAccountsClient = fxaClient; michael@0: michael@0: configureFxAccountIdentity(browseridManager, identityConfig); michael@0: michael@0: // Ensure the new FxAccounts mock has a signed-in user. michael@0: fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; michael@0: michael@0: browseridManager._fxaService = fxa; michael@0: michael@0: do_check_eq(browseridManager._fxaService.internal.now(), now); michael@0: michael@0: let request = new SyncStorageRequest("https://example.net/i/like/pie/"); michael@0: let authenticator = browseridManager.getResourceAuthenticator(); michael@0: let output = authenticator(request, 'GET'); michael@0: dump("output" + JSON.stringify(output)); michael@0: let authHeader = output.headers.authorization; michael@0: do_check_true(authHeader.startsWith('Hawk')); michael@0: michael@0: // Skew correction is applied in the header and we're within the two-minute michael@0: // window. michael@0: do_check_eq(getTimestamp(authHeader), now - 12 * HOUR_MS); michael@0: do_check_true( michael@0: (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_task(function test_ensureLoggedIn() { michael@0: configureFxAccountIdentity(browseridManager); michael@0: yield browseridManager.initializeWithCurrentIdentity(); michael@0: Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked"); michael@0: yield browseridManager.ensureLoggedIn(); michael@0: Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked"); michael@0: Assert.ok(browseridManager._shouldHaveSyncKeyBundle, michael@0: "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); michael@0: michael@0: // arrange for no logged in user. michael@0: let fxa = browseridManager._fxaService michael@0: let signedInUser = fxa.internal.currentAccountState.signedInUser; michael@0: fxa.internal.currentAccountState.signedInUser = null; michael@0: browseridManager.initializeWithCurrentIdentity(); michael@0: Assert.ok(!browseridManager._shouldHaveSyncKeyBundle, michael@0: "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are."); michael@0: Status.login = LOGIN_FAILED_NO_USERNAME; michael@0: yield Assert_rejects(browseridManager.ensureLoggedIn(), "expecting rejection due to no user"); michael@0: Assert.ok(browseridManager._shouldHaveSyncKeyBundle, michael@0: "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes."); michael@0: fxa.internal.currentAccountState.signedInUser = signedInUser; michael@0: Status.login = LOGIN_FAILED_LOGIN_REJECTED; michael@0: yield Assert_rejects(browseridManager.ensureLoggedIn(), michael@0: "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection"); michael@0: Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, michael@0: "status should remain LOGIN_FAILED_LOGIN_REJECTED"); michael@0: Status.login = LOGIN_FAILED_NETWORK_ERROR; michael@0: yield browseridManager.ensureLoggedIn(); michael@0: Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked"); michael@0: }); michael@0: michael@0: add_test(function test_tokenExpiration() { michael@0: _("BrowserIDManager notices token expiration:"); michael@0: let bimExp = new BrowserIDManager(); michael@0: configureFxAccountIdentity(bimExp, identityConfig); michael@0: michael@0: let authenticator = bimExp.getResourceAuthenticator(); michael@0: do_check_true(!!authenticator); michael@0: let req = {uri: CommonUtils.makeURI( michael@0: "https://example.net/somewhere/over/the/rainbow"), michael@0: method: 'GET'}; michael@0: authenticator(req, 'GET'); michael@0: michael@0: // Mock the clock. michael@0: _("Forcing the token to expire ..."); michael@0: Object.defineProperty(bimExp, "_now", { michael@0: value: function customNow() { michael@0: return (Date.now() + 3000001); michael@0: }, michael@0: writable: true, michael@0: }); michael@0: do_check_true(bimExp._token.expiration < bimExp._now()); michael@0: _("... means BrowserIDManager knows to re-fetch it on the next call."); michael@0: do_check_false(bimExp.hasValidToken()); michael@0: run_next_test(); michael@0: } michael@0: ); michael@0: michael@0: add_test(function test_sha256() { michael@0: // Test vectors from http://www.bichlmeier.info/sha256test.html michael@0: let vectors = [ michael@0: ["", michael@0: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"], michael@0: ["abc", michael@0: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"], michael@0: ["message digest", michael@0: "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650"], michael@0: ["secure hash algorithm", michael@0: "f30ceb2bb2829e79e4ca9753d35a8ecc00262d164cc077080295381cbd643f0d"], michael@0: ["SHA256 is considered to be safe", michael@0: "6819d915c73f4d1e77e4e1b52d1fa0f9cf9beaead3939f15874bd988e2a23630"], michael@0: ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", michael@0: "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"], michael@0: ["For this sample, this 63-byte string will be used as input data", michael@0: "f08a78cbbaee082b052ae0708f32fa1e50c5c421aa772ba5dbb406a2ea6be342"], michael@0: ["This is exactly 64 bytes long, not counting the terminating byte", michael@0: "ab64eff7e88e2e46165e29f2bce41826bd4c7b3552f6b382a9e7d3af47c245f8"] michael@0: ]; michael@0: let bidUser = new BrowserIDManager(); michael@0: for (let [input,output] of vectors) { michael@0: do_check_eq(CommonUtils.bytesAsHex(bidUser._sha256(input)), output); michael@0: } michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_computeXClientStateHeader() { michael@0: let kBhex = "fd5c747806c07ce0b9d69dcfea144663e630b65ec4963596a22f24910d7dd15d"; michael@0: let kB = CommonUtils.hexToBytes(kBhex); michael@0: michael@0: let bidUser = new BrowserIDManager(); michael@0: let header = bidUser._computeXClientState(kB); michael@0: michael@0: do_check_eq(header, "6ae94683571c7a7c54dab4700aa3995f"); michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_task(function test_getTokenErrors() { michael@0: _("BrowserIDManager correctly handles various failures to get a token."); michael@0: michael@0: _("Arrange for a 401 - Sync should reflect an auth error."); michael@0: initializeIdentityWithTokenServerResponse({ michael@0: status: 401, michael@0: headers: {"content-type": "application/json"}, michael@0: body: JSON.stringify({}), michael@0: }); michael@0: let browseridManager = Service.identity; michael@0: michael@0: yield browseridManager.initializeWithCurrentIdentity(); michael@0: yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, michael@0: "should reject due to 401"); michael@0: Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); michael@0: michael@0: // XXX - other interesting responses to return? michael@0: michael@0: // And for good measure, some totally "unexpected" errors - we generally michael@0: // assume these problems are going to magically go away at some point. michael@0: _("Arrange for an empty body with a 200 response - should reflect a network error."); michael@0: initializeIdentityWithTokenServerResponse({ michael@0: status: 200, michael@0: headers: [], michael@0: body: "", michael@0: }); michael@0: browseridManager = Service.identity; michael@0: yield browseridManager.initializeWithCurrentIdentity(); michael@0: yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, michael@0: "should reject due to non-JSON response"); michael@0: Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); michael@0: }); michael@0: michael@0: add_task(function test_getTokenErrorWithRetry() { michael@0: _("tokenserver sends an observer notification on various backoff headers."); michael@0: michael@0: // Set Sync's backoffInterval to zero - after we simulated the backoff header michael@0: // it should reflect the value we sent. michael@0: Status.backoffInterval = 0; michael@0: _("Arrange for a 503 with a Retry-After header."); michael@0: initializeIdentityWithTokenServerResponse({ michael@0: status: 503, michael@0: headers: {"content-type": "application/json", michael@0: "retry-after": "100"}, michael@0: body: JSON.stringify({}), michael@0: }); michael@0: let browseridManager = Service.identity; michael@0: michael@0: yield browseridManager.initializeWithCurrentIdentity(); michael@0: yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, michael@0: "should reject due to 503"); michael@0: michael@0: // The observer should have fired - check it got the value in the response. michael@0: Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); michael@0: // Sync will have the value in ms with some slop - so check it is at least that. michael@0: Assert.ok(Status.backoffInterval >= 100000); michael@0: michael@0: _("Arrange for a 200 with an X-Backoff header."); michael@0: Status.backoffInterval = 0; michael@0: initializeIdentityWithTokenServerResponse({ michael@0: status: 503, michael@0: headers: {"content-type": "application/json", michael@0: "x-backoff": "200"}, michael@0: body: JSON.stringify({}), michael@0: }); michael@0: browseridManager = Service.identity; michael@0: michael@0: yield browseridManager.initializeWithCurrentIdentity(); michael@0: yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, michael@0: "should reject due to no token in response"); michael@0: michael@0: // The observer should have fired - check it got the value in the response. michael@0: Assert.ok(Status.backoffInterval >= 200000); michael@0: }); michael@0: michael@0: add_task(function test_getKeysErrorWithBackoff() { michael@0: _("Auth server (via hawk) sends an observer notification on backoff headers."); michael@0: michael@0: // Set Sync's backoffInterval to zero - after we simulated the backoff header michael@0: // it should reflect the value we sent. michael@0: Status.backoffInterval = 0; michael@0: _("Arrange for a 503 with a X-Backoff header."); michael@0: michael@0: let config = makeIdentityConfig(); michael@0: // We want no kA or kB so we attempt to fetch them. michael@0: delete config.fxaccount.user.kA; michael@0: delete config.fxaccount.user.kB; michael@0: config.fxaccount.user.keyFetchToken = "keyfetchtoken"; michael@0: yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { michael@0: Assert.equal(method, "get"); michael@0: Assert.equal(uri, "http://mockedserver:9999/account/keys") michael@0: return { michael@0: status: 503, michael@0: headers: {"content-type": "application/json", michael@0: "x-backoff": "100"}, michael@0: body: "{}", michael@0: } michael@0: }); michael@0: michael@0: let browseridManager = Service.identity; michael@0: yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, michael@0: "should reject due to 503"); michael@0: michael@0: // The observer should have fired - check it got the value in the response. michael@0: Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); michael@0: // Sync will have the value in ms with some slop - so check it is at least that. michael@0: Assert.ok(Status.backoffInterval >= 100000); michael@0: }); michael@0: michael@0: add_task(function test_getKeysErrorWithRetry() { michael@0: _("Auth server (via hawk) sends an observer notification on retry headers."); michael@0: michael@0: // Set Sync's backoffInterval to zero - after we simulated the backoff header michael@0: // it should reflect the value we sent. michael@0: Status.backoffInterval = 0; michael@0: _("Arrange for a 503 with a Retry-After header."); michael@0: michael@0: let config = makeIdentityConfig(); michael@0: // We want no kA or kB so we attempt to fetch them. michael@0: delete config.fxaccount.user.kA; michael@0: delete config.fxaccount.user.kB; michael@0: config.fxaccount.user.keyFetchToken = "keyfetchtoken"; michael@0: yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { michael@0: Assert.equal(method, "get"); michael@0: Assert.equal(uri, "http://mockedserver:9999/account/keys") michael@0: return { michael@0: status: 503, michael@0: headers: {"content-type": "application/json", michael@0: "retry-after": "100"}, michael@0: body: "{}", michael@0: } michael@0: }); michael@0: michael@0: let browseridManager = Service.identity; michael@0: yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, michael@0: "should reject due to 503"); michael@0: michael@0: // The observer should have fired - check it got the value in the response. michael@0: Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); michael@0: // Sync will have the value in ms with some slop - so check it is at least that. michael@0: Assert.ok(Status.backoffInterval >= 100000); michael@0: }); michael@0: michael@0: add_task(function test_getHAWKErrors() { michael@0: _("BrowserIDManager correctly handles various HAWK failures."); michael@0: michael@0: _("Arrange for a 401 - Sync should reflect an auth error."); michael@0: let config = makeIdentityConfig(); michael@0: yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { michael@0: Assert.equal(method, "post"); michael@0: Assert.equal(uri, "http://mockedserver:9999/certificate/sign") michael@0: return { michael@0: status: 401, michael@0: headers: {"content-type": "application/json"}, michael@0: body: JSON.stringify({}), michael@0: } michael@0: }); michael@0: Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); michael@0: michael@0: // XXX - other interesting responses to return? michael@0: michael@0: // And for good measure, some totally "unexpected" errors - we generally michael@0: // assume these problems are going to magically go away at some point. michael@0: _("Arrange for an empty body with a 200 response - should reflect a network error."); michael@0: yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { michael@0: Assert.equal(method, "post"); michael@0: Assert.equal(uri, "http://mockedserver:9999/certificate/sign") michael@0: return { michael@0: status: 200, michael@0: headers: [], michael@0: body: "", michael@0: } michael@0: }); michael@0: Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); michael@0: }); michael@0: michael@0: add_task(function test_getGetKeysFailing401() { michael@0: _("BrowserIDManager correctly handles 401 responses fetching keys."); michael@0: michael@0: _("Arrange for a 401 - Sync should reflect an auth error."); michael@0: let config = makeIdentityConfig(); michael@0: // We want no kA or kB so we attempt to fetch them. michael@0: delete config.fxaccount.user.kA; michael@0: delete config.fxaccount.user.kB; michael@0: config.fxaccount.user.keyFetchToken = "keyfetchtoken"; michael@0: yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { michael@0: Assert.equal(method, "get"); michael@0: Assert.equal(uri, "http://mockedserver:9999/account/keys") michael@0: return { michael@0: status: 401, michael@0: headers: {"content-type": "application/json"}, michael@0: body: "{}", michael@0: } michael@0: }); michael@0: Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); michael@0: }); michael@0: michael@0: add_task(function test_getGetKeysFailing503() { michael@0: _("BrowserIDManager correctly handles 5XX responses fetching keys."); michael@0: michael@0: _("Arrange for a 503 - Sync should reflect a network error."); michael@0: let config = makeIdentityConfig(); michael@0: // We want no kA or kB so we attempt to fetch them. michael@0: delete config.fxaccount.user.kA; michael@0: delete config.fxaccount.user.kB; michael@0: config.fxaccount.user.keyFetchToken = "keyfetchtoken"; michael@0: yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { michael@0: Assert.equal(method, "get"); michael@0: Assert.equal(uri, "http://mockedserver:9999/account/keys") michael@0: return { michael@0: status: 503, michael@0: headers: {"content-type": "application/json"}, michael@0: body: "{}", michael@0: } michael@0: }); michael@0: Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error"); michael@0: }); michael@0: michael@0: add_task(function test_getKeysMissing() { michael@0: _("BrowserIDManager correctly handles getKeys succeeding but not returning keys."); michael@0: michael@0: let browseridManager = new BrowserIDManager(); michael@0: let identityConfig = makeIdentityConfig(); michael@0: // our mock identity config already has kA and kB - remove them or we never michael@0: // try and fetch them. michael@0: delete identityConfig.fxaccount.user.kA; michael@0: delete identityConfig.fxaccount.user.kB; michael@0: identityConfig.fxaccount.user.keyFetchToken = 'keyFetchToken'; michael@0: michael@0: configureFxAccountIdentity(browseridManager, identityConfig); michael@0: michael@0: // Mock a fxAccounts object that returns no keys michael@0: let fxa = new FxAccounts({ michael@0: fetchAndUnwrapKeys: function () { michael@0: return Promise.resolve({}); michael@0: }, michael@0: fxAccountsClient: new MockFxAccountsClient() michael@0: }); michael@0: michael@0: // Add a mock to the currentAccountState object. michael@0: fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) { michael@0: this.cert = { michael@0: validUntil: fxa.internal.now() + CERT_LIFETIME, michael@0: cert: "certificate", michael@0: }; michael@0: return Promise.resolve(this.cert.cert); michael@0: }; michael@0: michael@0: // Ensure the new FxAccounts mock has a signed-in user. michael@0: fxa.internal.currentAccountState.signedInUser = browseridManager._fxaService.internal.currentAccountState.signedInUser; michael@0: michael@0: browseridManager._fxaService = fxa; michael@0: michael@0: yield browseridManager.initializeWithCurrentIdentity(); michael@0: michael@0: let ex; michael@0: try { michael@0: yield browseridManager.whenReadyToAuthenticate.promise; michael@0: } catch (e) { michael@0: ex = e; michael@0: } michael@0: michael@0: Assert.ok(ex.message.indexOf("missing kA or kB") >= 0); michael@0: }); michael@0: michael@0: // End of tests michael@0: // Utility functions follow michael@0: michael@0: // Create a new browserid_identity object and initialize it with a michael@0: // hawk mock that simulates HTTP responses. michael@0: // The callback function will be called each time the mocked hawk server wants michael@0: // to make a request. The result of the callback should be the mock response michael@0: // object that will be returned to hawk. michael@0: // A token server mock will be used that doesn't hit a server, so we move michael@0: // directly to a hawk request. michael@0: function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) { michael@0: // A mock request object. michael@0: function MockRESTRequest(uri, credentials, extra) { michael@0: this._uri = uri; michael@0: this._credentials = credentials; michael@0: this._extra = extra; michael@0: }; michael@0: MockRESTRequest.prototype = { michael@0: setHeader: function() {}, michael@0: post: function(data, callback) { michael@0: this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra); michael@0: callback.call(this); michael@0: }, michael@0: get: function(callback) { michael@0: this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra); michael@0: callback.call(this); michael@0: } michael@0: } michael@0: michael@0: // The hawk client. michael@0: function MockedHawkClient() {} michael@0: MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999"); michael@0: MockedHawkClient.prototype.constructor = MockedHawkClient; michael@0: MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) { michael@0: return new MockRESTRequest(uri, credentials, extra); michael@0: } michael@0: // Arrange for the same observerPrefix as FxAccountsClient uses michael@0: MockedHawkClient.prototype.observerPrefix = "FxA:hawk"; michael@0: michael@0: // tie it all together - configureFxAccountIdentity isn't useful here :( michael@0: let fxaClient = new MockFxAccountsClient(); michael@0: fxaClient.hawk = new MockedHawkClient(); michael@0: let internal = { michael@0: fxAccountsClient: fxaClient, michael@0: } michael@0: let fxa = new FxAccounts(internal); michael@0: fxa.internal.currentAccountState.signedInUser = { michael@0: accountData: config.fxaccount.user, michael@0: }; michael@0: michael@0: browseridManager._fxaService = fxa; michael@0: browseridManager._signedInUser = null; michael@0: yield browseridManager.initializeWithCurrentIdentity(); michael@0: yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, michael@0: "expecting rejection due to hawk error"); michael@0: } michael@0: michael@0: michael@0: function getTimestamp(hawkAuthHeader) { michael@0: return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS; michael@0: } michael@0: michael@0: function getTimestampDelta(hawkAuthHeader, now=Date.now()) { michael@0: return Math.abs(getTimestamp(hawkAuthHeader) - now); michael@0: } michael@0: