Bug 1065153 - Support call URLs with guest or FxA hawk session types, r=MattN
authorDan Mosedale <dmose@meer.net>
Thu, 18 Sep 2014 10:40:35 -0700
changeset 206140 b541be2a5459b70bae82bb8ec95b0fd687e12cf3
parent 206139 d1ae4274cf8bc078125616da4ad4b190989f6034
child 206141 5710731f09e99074e79ed0c4420b2598dd42f535
push id27514
push usercbook@mozilla.com
push dateFri, 19 Sep 2014 12:24:09 +0000
treeherdermozilla-central@3475e6a1665a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1065153
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1065153 - Support call URLs with guest or FxA hawk session types, r=MattN
browser/components/loop/LoopStorage.jsm
browser/components/loop/MozLoopAPI.jsm
browser/components/loop/MozLoopService.jsm
browser/components/loop/content/js/client.js
browser/components/loop/test/desktop-local/client_test.js
browser/components/loop/test/xpcshell/test_loopapi_hawk_request.js
browser/components/loop/test/xpcshell/xpcshell.ini
--- a/browser/components/loop/LoopStorage.jsm
+++ b/browser/components/loop/LoopStorage.jsm
@@ -1,16 +1,25 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-Cu.importGlobalProperties(["indexedDB"]);
+// Make it possible to load LoopStorage.jsm in xpcshell tests
+try {
+  Cu.importGlobalProperties(["indexedDB"]);
+} catch (ex) {
+  // don't write this is out in xpcshell, since it's expected there
+  if (typeof window !== 'undefined' && "console" in window) {
+    console.log("Failed to import indexedDB; if this isn't a unit test," +
+                " something is wrong", ex);
+  }
+}
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
   const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
   return new EventEmitter();
 });
 
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -110,16 +110,18 @@ function injectLoopAPI(targetWindow) {
   let ringerStopper;
   let appVersionInfo;
   let contactsAPI;
 
   let api = {
     /**
      * Gets an object with data that represents the currently
      * authenticated user's identity.
+     *
+     * @return null if user not logged in; profile object otherwise
      */
     userProfile: {
       enumerable: true,
       get: function() {
         if (!MozLoopService.userProfile)
           return null;
         let userProfile = Cu.cloneInto({
           email: MozLoopService.userProfile.email,
@@ -373,29 +375,31 @@ function injectLoopAPI(targetWindow) {
      *    {
      *      code: 401,
      *      errno: 401,
      *      error: "Request failed",
      *      message: "invalid token"
      *    }
      *  - {String} The body of the response.
      *
+     * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for
+     *                                        the request.  This is one of the
+     *                                        LOOP_SESSION_TYPE members
      * @param {String} path The path to make the request to.
      * @param {String} method The request method, e.g. 'POST', 'GET'.
      * @param {Object} payloadObj An object which is converted to JSON and
      *                            transmitted with the request.
      * @param {Function} callback Called when the request completes.
      */
     hawkRequest: {
       enumerable: true,
       writable: true,
-      value: function(path, method, payloadObj, callback) {
-        // XXX: Bug 1065153 - Should take a sessionType parameter instead of hard-coding GUEST
+      value: function(sessionType, path, method, payloadObj, callback) {
         // XXX Should really return a DOM promise here.
-        MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => {
+        MozLoopService.hawkRequest(sessionType, path, method, payloadObj).then((response) => {
           callback(null, response.body);
         }, hawkError => {
           // The hawkError.error property, while usually a string representing
           // an HTTP response status message, may also incorrectly be a native
           // error object that will cause the cloning function to fail.
           callback(Cu.cloneInto({
             error: (hawkError.error && typeof hawkError.error == "string")
                    ? hawkError.error : "Unexpected exception",
@@ -404,20 +408,19 @@ function injectLoopAPI(targetWindow) {
             errno: hawkError.errno,
           }, targetWindow));
         }).catch(Cu.reportError);
       }
     },
 
     LOOP_SESSION_TYPE: {
       enumerable: true,
-      writable: false,
-      value: function() {
-        return LOOP_SESSION_TYPE;
-      },
+      get: function() {
+        return Cu.cloneInto(LOOP_SESSION_TYPE, targetWindow);
+      }
     },
 
     logInToFxA: {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.logInToFxA();
       }
@@ -445,21 +448,31 @@ function injectLoopAPI(targetWindow) {
      *   - OS: The operating system the application is running on
      */
     appVersionInfo: {
       enumerable: true,
       get: function() {
         if (!appVersionInfo) {
           let defaults = Services.prefs.getDefaultBranch(null);
 
-          appVersionInfo = Cu.cloneInto({
-            channel: defaults.getCharPref("app.update.channel"),
-            version: appInfo.version,
-            OS: appInfo.OS
-          }, targetWindow);
+          // If the lazy getter explodes, we're probably loaded in xpcshell,
+          // which doesn't have what we need, so log an error.
+          try {
+            appVersionInfo = Cu.cloneInto({
+              channel: defaults.getCharPref("app.update.channel"),
+              version: appInfo.version,
+              OS: appInfo.OS
+            }, targetWindow);
+          } catch (ex) {
+            // only log outside of xpcshell to avoid extra message noise
+            if (typeof window !== 'undefined' && "console" in window) {
+              console.log("Failed to construct appVersionInfo; if this isn't " +
+                          "an xpcshell unit test, something is wrong", ex);
+            }
+          }
         }
         return appVersionInfo;
       }
     },
 
     /**
      * Composes an email via the external protocol service.
      *
@@ -505,27 +518,33 @@ function injectLoopAPI(targetWindow) {
 
   let contentObj = Cu.createObjectIn(targetWindow);
   Object.defineProperties(contentObj, api);
   Object.seal(contentObj);
   Cu.makeObjectPropsNormal(contentObj);
   Services.obs.addObserver(onStatusChanged, "loop-status-changed", false);
   Services.obs.addObserver(onDOMWindowDestroyed, "dom-window-destroyed", false);
 
-  targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function() {
-    // We do this in a getter, so that we create these objects
-    // only on demand (this is a potential concern, since
-    // otherwise we might add one per iframe, and keep them
-    // alive for as long as the window is alive).
-    delete targetWindow.navigator.wrappedJSObject.mozLoop;
-    return targetWindow.navigator.wrappedJSObject.mozLoop = contentObj;
-  });
+  if ("navigator" in targetWindow) {
+    targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function () {
+      // We do this in a getter, so that we create these objects
+      // only on demand (this is a potential concern, since
+      // otherwise we might add one per iframe, and keep them
+      // alive for as long as the window is alive).
+      delete targetWindow.navigator.wrappedJSObject.mozLoop;
+      return targetWindow.navigator.wrappedJSObject.mozLoop = contentObj;
+    });
 
-  // Handle window.close correctly on the panel and chatbox.
-  hookWindowCloseForPanelClose(targetWindow);
+    // Handle window.close correctly on the panel and chatbox.
+    hookWindowCloseForPanelClose(targetWindow);
+  } else {
+    // This isn't a window; but it should be a JS scope; used for testing
+    return targetWindow.mozLoop = contentObj;
+  }
+
 }
 
 function getChromeWindow(contentWin) {
   return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIWebNavigation)
                    .QueryInterface(Ci.nsIDocShellTreeItem)
                    .rootTreeItem
                    .QueryInterface(Ci.nsIInterfaceRequestor)
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -698,16 +698,21 @@ this.MozLoopService = {
     gInitializeTimerFunc = value;
   },
 
   /**
    * Initialized the loop service, and starts registration with the
    * push and loop servers.
    */
   initialize: function() {
+
+    // Do this here, rather than immediately after definition, so that we can
+    // stub out API functions for unit testing
+    Object.freeze(this);
+
     // Don't do anything if loop is not enabled.
     if (!Services.prefs.getBoolPref("loop.enabled") ||
         Services.prefs.getBoolPref("loop.throttled")) {
       return;
     }
 
     // If expiresTime is in the future then kick-off registration.
     if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
@@ -1047,9 +1052,8 @@ this.MozLoopService = {
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
   hawkRequest: function(sessionType, path, method, payloadObj) {
     return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj);
   },
 };
-Object.freeze(this.MozLoopService);
--- a/browser/components/loop/content/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -103,39 +103,47 @@ loop.Client = (function($) {
      * - callUrlData an object of the obtained call url data if successful:
      * -- callUrl: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
      * @param  {string} nickname the nickname of the future caller
      * @param  {Function} cb Callback(err, callUrlData)
      */
     _requestCallUrlInternal: function(nickname, cb) {
-      this.mozLoop.hawkRequest("/call-url/", "POST", {callerId: nickname},
-                               function (error, responseText) {
-        if (error) {
-          this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false);
-          this._failureHandler(cb, error);
-          return;
-        }
-
-        try {
-          var urlData = JSON.parse(responseText);
+      var sessionType;
+      if (this.mozLoop.userProfile) {
+        sessionType = this.mozLoop.LOOP_SESSION_TYPE.FXA;
+      } else {
+        sessionType = this.mozLoop.LOOP_SESSION_TYPE.GUEST;
+      }
+      
+      this.mozLoop.hawkRequest(sessionType, "/call-url/", "POST",
+                               {callerId: nickname},
+        function (error, responseText) {
+          if (error) {
+            this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false);
+            this._failureHandler(cb, error);
+            return;
+          }
 
-          // This throws if the data is invalid, in which case only the failure
-          // telementry will be recorded.
-          var returnData = this._validate(urlData, expectedCallUrlProperties);
+          try {
+            var urlData = JSON.parse(responseText);
+
+            // This throws if the data is invalid, in which case only the failure
+            // telemetry will be recorded.
+            var returnData = this._validate(urlData, expectedCallUrlProperties);
 
-          this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", true);
-          cb(null, returnData);
-        } catch (err) {
-          this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false);
-          console.log("Error requesting call info", err);
-          cb(err);
-        }
-      }.bind(this));
+            this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", true);
+            cb(null, returnData);
+          } catch (err) {
+            this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false);
+            console.log("Error requesting call info", err);
+            cb(err);
+          }
+        }.bind(this));
     },
 
     /**
      * Block call URL based on the token identifier
      *
      * @param {string} token Conversation identifier used to block the URL
      * @param {function} cb Callback function used for handling an error
      *                      response. XXX The incoming call panel does not
@@ -149,35 +157,41 @@ loop.Client = (function($) {
           return;
         }
 
         this._deleteCallUrlInternal(token, cb);
       }.bind(this));
     },
 
     _deleteCallUrlInternal: function(token, cb) {
-      this.mozLoop.hawkRequest("/call-url/" + token, "DELETE", null,
-                               function (error, responseText) {
+      function deleteRequestCallback(error, responseText) {
         if (error) {
           this._failureHandler(cb, error);
           return;
         }
 
         try {
           cb(null);
         } catch (err) {
           console.log("Error deleting call info", err);
           cb(err);
         }
-      }.bind(this));
+      }
+
+      // XXX hard-coding of GUEST to be removed by 1065155
+      this.mozLoop.hawkRequest(this.mozLoop.LOOP_SESSION_TYPE.GUEST,
+                               "/call-url/" + token, "DELETE", null,
+                               deleteRequestCallback.bind(this));
     },
 
     /**
      * Requests a call URL from the Loop server. It will note the
-     * expiry time for the url with the mozLoop api.
+     * expiry time for the url with the mozLoop api.  It will select the
+     * appropriate hawk session to use based on whether or not the user
+     * is currently logged into a Firefox account profile.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      * - callUrlData an object of the obtained call url data if successful:
      * -- callUrl: The url of the call
      * -- expiresAt: The amount of hours until expiry of the url
      *
      * @param  {String} simplepushUrl a registered Simple Push URL
--- a/browser/components/loop/test/desktop-local/client_test.js
+++ b/browser/components/loop/test/desktop-local/client_test.js
@@ -30,17 +30,22 @@ describe("loop.Client", function() {
     mozLoop = {
       getLoopCharPref: sandbox.stub()
         .returns(null)
         .withArgs("hawk-session-token")
         .returns(fakeToken),
       ensureRegistered: sinon.stub().callsArgWith(0, null),
       noteCallUrlExpiry: sinon.spy(),
       hawkRequest: sinon.stub(),
-      telemetryAdd: sinon.spy(),
+      LOOP_SESSION_TYPE: {
+        GUEST: 1,
+        FXA: 2
+      },
+      userProfile: null,
+      telemetryAdd: sinon.spy()
     };
     // Alias for clearer tests.
     hawkRequestStub = mozLoop.hawkRequest;
     client = new loop.Client({
       mozLoop: mozLoop
     });
   });
 
@@ -65,35 +70,36 @@ describe("loop.Client", function() {
         sinon.assert.calledWithExactly(callback, "offline");
       });
 
       it("should make a delete call to /call-url/{fakeToken}", function() {
         client.deleteCallUrl(fakeToken, callback);
 
         sinon.assert.calledOnce(hawkRequestStub);
         sinon.assert.calledWith(hawkRequestStub,
+                                mozLoop.LOOP_SESSION_TYPE.GUEST,
                                 "/call-url/" + fakeToken, "DELETE");
       });
 
       it("should call the callback with null when the request succeeds",
          function() {
 
            // Sets up the hawkRequest stub to trigger the callback with no error
            // and the url.
-           hawkRequestStub.callsArgWith(3, null);
+           hawkRequestStub.callsArgWith(4, null);
 
            client.deleteCallUrl(fakeToken, callback);
 
            sinon.assert.calledWithExactly(callback, null);
          });
 
       it("should send an error when the request fails", function() {
         // Sets up the hawkRequest stub to trigger the callback with
         // an error
-        hawkRequestStub.callsArgWith(3, fakeErrorRes);
+        hawkRequestStub.callsArgWith(4, fakeErrorRes);
 
         client.deleteCallUrl(fakeToken, callback);
 
         sinon.assert.calledOnce(callback);
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return /400.*invalid token/.test(err.message);
         }));
       });
@@ -114,64 +120,86 @@ describe("loop.Client", function() {
         sinon.assert.calledOnce(callback);
         sinon.assert.calledWithExactly(callback, "offline");
       });
 
       it("should post to /call-url/", function() {
         client.requestCallUrl("foo", callback);
 
         sinon.assert.calledOnce(hawkRequestStub);
-        sinon.assert.calledWith(hawkRequestStub,
-                                "/call-url/", "POST", {callerId: "foo"});
+        sinon.assert.calledWithExactly(hawkRequestStub, sinon.match.number,
+          "/call-url/", "POST", {callerId: "foo"}, sinon.match.func);
+      });
+
+      it("should send a sessionType of LOOP_SESSION_TYPE.GUEST when " +
+         "mozLoop.userProfile returns null", function() {
+        mozLoop.userProfile = null;
+
+        client.requestCallUrl("foo", callback);
+
+        sinon.assert.calledOnce(hawkRequestStub);
+        sinon.assert.calledWithExactly(hawkRequestStub,
+          mozLoop.LOOP_SESSION_TYPE.GUEST, "/call-url/", "POST",
+          {callerId: "foo"}, sinon.match.func);
+      });
+
+      it("should send a sessionType of LOOP_SESSION_TYPE.FXA when " +
+         "mozLoop.userProfile returns an object", function () {
+        mozLoop.userProfile = {};
+
+        client.requestCallUrl("foo", callback);
+
+        sinon.assert.calledOnce(hawkRequestStub);
+        sinon.assert.calledWithExactly(hawkRequestStub,
+          mozLoop.LOOP_SESSION_TYPE.FXA, "/call-url/", "POST",
+          {callerId: "foo"}, sinon.match.func);
       });
 
       it("should call the callback with the url when the request succeeds",
         function() {
           var callUrlData = {
             "callUrl": "fakeCallUrl",
             "expiresAt": 60
           };
 
           // Sets up the hawkRequest stub to trigger the callback with no error
           // and the url.
-          hawkRequestStub.callsArgWith(3, null,
-            JSON.stringify(callUrlData));
+          hawkRequestStub.callsArgWith(4, null, JSON.stringify(callUrlData));
 
           client.requestCallUrl("foo", callback);
 
           sinon.assert.calledWithExactly(callback, null, callUrlData);
         });
 
       it("should not update call url expiry when the request succeeds",
         function() {
           var callUrlData = {
             "callUrl": "fakeCallUrl",
             "expiresAt": 6000
           };
 
           // Sets up the hawkRequest stub to trigger the callback with no error
           // and the url.
-          hawkRequestStub.callsArgWith(3, null,
-            JSON.stringify(callUrlData));
+          hawkRequestStub.callsArgWith(4, null, JSON.stringify(callUrlData));
 
           client.requestCallUrl("foo", callback);
 
           sinon.assert.notCalled(mozLoop.noteCallUrlExpiry);
         });
 
       it("should call mozLoop.telemetryAdd when the request succeeds",
         function(done) {
           var callUrlData = {
             "callUrl": "fakeCallUrl",
             "expiresAt": 60
           };
 
           // Sets up the hawkRequest stub to trigger the callback with no error
           // and the url.
-          hawkRequestStub.callsArgWith(3, null,
+          hawkRequestStub.callsArgWith(4, null,
             JSON.stringify(callUrlData));
 
           client.requestCallUrl("foo", function(err) {
             expect(err).to.be.null;
 
             sinon.assert.calledOnce(mozLoop.telemetryAdd);
             sinon.assert.calledWith(mozLoop.telemetryAdd,
                                     "LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS",
@@ -179,44 +207,44 @@ describe("loop.Client", function() {
 
             done();
           });
         });
 
       it("should send an error when the request fails", function() {
         // Sets up the hawkRequest stub to trigger the callback with
         // an error
-        hawkRequestStub.callsArgWith(3, fakeErrorRes);
+        hawkRequestStub.callsArgWith(4, fakeErrorRes);
 
         client.requestCallUrl("foo", callback);
 
         sinon.assert.calledOnce(callback);
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return /400.*invalid token/.test(err.message);
         }));
       });
 
       it("should send an error if the data is not valid", function() {
         // Sets up the hawkRequest stub to trigger the callback with
         // an error
-        hawkRequestStub.callsArgWith(3, null, "{}");
+        hawkRequestStub.callsArgWith(4, null, "{}");
 
         client.requestCallUrl("foo", callback);
 
         sinon.assert.calledOnce(callback);
         sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
           return /Invalid data received/.test(err.message);
         }));
       });
 
       it("should call mozLoop.telemetryAdd when the request fails",
         function(done) {
           // Sets up the hawkRequest stub to trigger the callback with
           // an error
-          hawkRequestStub.callsArgWith(3, fakeErrorRes);
+          hawkRequestStub.callsArgWith(4, fakeErrorRes);
 
           client.requestCallUrl("foo", function(err) {
             expect(err).not.to.be.null;
 
             sinon.assert.calledOnce(mozLoop.telemetryAdd);
             sinon.assert.calledWith(mozLoop.telemetryAdd,
                                     "LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS",
                                     false);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_loopapi_hawk_request.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Unit tests for the hawkRequest API
+ */
+
+"use strict";
+
+Cu.import("resource:///modules/loop/MozLoopAPI.jsm");
+
+let sandbox;
+function assertInSandbox(expr, msg_opt) {
+  Assert.ok(Cu.evalInSandbox(expr, sandbox), msg_opt);
+}
+
+sandbox = Cu.Sandbox("about:looppanel", { wantXrays: false } );
+injectLoopAPI(sandbox, true);
+
+add_task(function* hawk_session_scope_constants() {
+  assertInSandbox("typeof mozLoop.LOOP_SESSION_TYPE !== 'undefined'");
+
+  assertInSandbox("mozLoop.LOOP_SESSION_TYPE.GUEST === 1");
+
+  assertInSandbox("mozLoop.LOOP_SESSION_TYPE.FXA === 2");
+});
+
+function generateSessionTypeVerificationStub(desiredSessionType) {
+
+  function hawkRequestStub(sessionType, path, method, payloadObj, callback) {
+    return new Promise(function (resolve, reject) {
+      Assert.equal(desiredSessionType, sessionType);
+
+      resolve();
+    });
+  };
+
+  return hawkRequestStub;
+}
+
+const origHawkRequest = MozLoopService.oldHawkRequest;
+do_register_cleanup(function() {
+  MozLoopService.hawkRequest = origHawkRequest;
+});
+
+add_task(function* hawk_request_scope_passthrough() {
+
+  // add a stub that verifies the parameter we want
+  MozLoopService.hawkRequest =
+    generateSessionTypeVerificationStub(sandbox.mozLoop.LOOP_SESSION_TYPE.FXA);
+
+  // call mozLoop.hawkRequest, which calls MozLoopAPI.hawkRequest, which calls
+  // MozLoopService.hawkRequest
+  Cu.evalInSandbox(
+    "mozLoop.hawkRequest(mozLoop.LOOP_SESSION_TYPE.FXA," +
+                       " 'call-url/fakeToken', 'POST', {}, function() {})",
+    sandbox);
+
+  MozLoopService.hawkRequest =
+    generateSessionTypeVerificationStub(sandbox.mozLoop.LOOP_SESSION_TYPE.GUEST);
+
+  Cu.evalInSandbox(
+    "mozLoop.hawkRequest(mozLoop.LOOP_SESSION_TYPE.GUEST," +
+    " 'call-url/fakeToken', 'POST', {}, function() {})",
+    sandbox);
+
+});
+
+function run_test() {
+  run_next_test();
+}
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
 
+[test_loopapi_hawk_request.js]
 [test_looppush_initialize.js]
 [test_loopservice_dnd.js]
 [test_loopservice_expiry.js]
 [test_loopservice_loop_prefs.js]
 [test_loopservice_initialize.js]
 [test_loopservice_locales.js]
 [test_loopservice_notification.js]
 [test_loopservice_registration.js]