Bug 977502 - Part 2: Add tests with mock http responses simulating errors. r=ckarlof, a=sledru
authorMark Hammond <mhammond@skippinet.com.au>
Fri, 07 Mar 2014 15:32:17 +1100
changeset 183240 bc4f7a9cd543133f1a9af4a4c080b9bf6c51db0b
parent 183239 23b00776e8f50ab8b92e764075218f66c197ae97
child 183241 dcc1a3b9741d319be3aec8229e7b8f29bf01c6aa
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersckarlof, sledru
bugs977502
milestone29.0a2
Bug 977502 - Part 2: Add tests with mock http responses simulating errors. r=ckarlof, a=sledru
services/common/hawk.js
services/common/tokenserverclient.js
services/sync/tests/unit/test_browserid_identity.js
--- a/services/common/hawk.js
+++ b/services/common/hawk.js
@@ -194,18 +194,24 @@ HawkClient.prototype = {
       deferred.resolve(this.response.body);
     };
 
     let extra = {
       now: this.now(),
       localtimeOffsetMsec: this.localtimeOffsetMsec,
     };
 
-    let request = new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
+    let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
     if (method == "post" || method == "put") {
       request[method](payloadObj, onComplete);
     } else {
       request[method](onComplete);
     }
 
     return deferred.promise;
-  }
+  },
+
+  // override points for testing.
+  newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) {
+    return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
+  },
+
 }
--- a/services/common/tokenserverclient.js
+++ b/services/common/tokenserverclient.js
@@ -240,17 +240,17 @@ TokenServerClient.prototype = {
     }
 
     if (!cb) {
       throw new TokenServerClientError("cb argument is not valid.");
     }
 
     this._log.debug("Beginning BID assertion exchange: " + url);
 
-    let req = new RESTRequest(url);
+    let req = this.newRESTRequest(url);
     req.setHeader("Accept", "application/json");
     req.setHeader("Authorization", "BrowserID " + assertion);
 
     for (let header in addHeaders) {
       req.setHeader(header, addHeaders[header]);
     }
 
     let client = this;
@@ -398,10 +398,15 @@ TokenServerClient.prototype = {
     this._log.debug("Successful token response: " + result.id);
     cb(null, {
       id:       result.id,
       key:      result.key,
       endpoint: result.api_endpoint,
       uid:      result.uid,
       duration: result.duration,
     });
+  },
+
+  // override points for testing.
+  newRESTRequest: function(url) {
+    return new RESTRequest(url);
   }
 };
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -7,16 +7,19 @@ Cu.import("resource://services-sync/rest
 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://services-common/hawk.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-common/tokenserverclient.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/constants.js");
 
 const SECOND_MS = 1000;
 const MINUTE_MS = SECOND_MS * 60;
 const HOUR_MS = MINUTE_MS * 60;
 
 let identityConfig = makeIdentityConfig();
 let browseridManager = new BrowserIDManager();
 configureFxAccountIdentity(browseridManager, identityConfig);
@@ -339,19 +342,152 @@ add_test(function test_computeXClientSta
 
   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.");
+  yield initializeIdentityWithTokenServerFailure({
+    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 initializeIdentityWithTokenServerFailure({
+    status: 200,
+    headers: [],
+    body: "",
+  });
+  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
+});
+
+add_task(function test_getHAWKErrors() {
+  _("BrowserIDManager correctly handles various HAWK failures.");
+
+  _("Arrange for a 401 - Sync should reflect an auth error.");
+  yield initializeIdentityWithHAWKFailure({
+    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 initializeIdentityWithHAWKFailure({
+    status: 200,
+    headers: [],
+    body: "",
+  });
+  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
+});
+
 // End of tests
 // Utility functions follow
 
+// Create a new browserid_identity object and initialize it with a
+// mocked TokenServerClient which always gets the specified response.
+function* initializeIdentityWithTokenServerFailure(response) {
+  // First create a mock "request" object that well' hack into the token server.
+  // A log for it
+  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;
+  }
+
+  // A mock request object.
+  function MockRESTRequest(url) {};
+  MockRESTRequest.prototype = {
+    _log: requestLog,
+    setHeader: function() {},
+    get: function(callback) {
+      this.response = response;
+      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);
+  }
+  // tie it all together.
+  let mockTSC = new MockTSC()
+  configureFxAccountIdentity(browseridManager);
+  browseridManager._tokenServerClient = mockTSC;
+
+  yield browseridManager.initializeWithCurrentIdentity();
+  try {
+    yield browseridManager.whenReadyToAuthenticate.promise;
+    Assert.ok(false, "expecting this promise to resolve with an error");
+  } catch (ex) {}
+}
+
+
+// Create a new browserid_identity object and initialize it with a
+// hawk mock that simulates a failure.
+// A token server mock will be used that doesn't hit a server, so we move
+// directly to a hawk request.
+function* initializeIdentityWithHAWKFailure(response) {
+  // A mock request object.
+  function MockRESTRequest() {};
+  MockRESTRequest.prototype = {
+    setHeader: function() {},
+    post: function(data, callback) {
+      this.response = response;
+      callback.call(this);
+    }
+  }
+
+  // The hawk client.
+  function MockedHawkClient() {}
+  MockedHawkClient.prototype = new HawkClient();
+  MockedHawkClient.prototype.constructor = MockedHawkClient;
+  MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) {
+    return new MockRESTRequest();
+  }
+
+  // tie it all together - configureFxAccountIdentity isn't useful here :(
+  let fxaClient = new MockFxAccountsClient();
+  fxaClient.hawk = new MockedHawkClient();
+  let config = makeIdentityConfig();
+  let internal = {
+    fxAccountsClient: fxaClient,
+  }
+  let fxa = new FxAccounts(internal);
+  fxa.internal.currentAccountState.signedInUser = {
+      accountData: config.fxaccount.user,
+  };
+
+  browseridManager._fxaService = fxa;
+  yield browseridManager.initializeWithCurrentIdentity();
+  try {
+    yield browseridManager.whenReadyToAuthenticate.promise;
+    Assert.ok(false, "expecting this promise to resolve with an error");
+  } catch (ex) {}
+}
+
+
 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);
 }