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