diff --git a/browser/components/loop/LoopStorage.jsm b/browser/components/loop/LoopStorage.jsm index f21a24e672c..f5df5ccbba8 100644 --- a/browser/components/loop/LoopStorage.jsm +++ b/browser/components/loop/LoopStorage.jsm @@ -5,7 +5,16 @@ 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"); diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index d5c1c76fceb..9048aaf0fde 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -115,6 +115,8 @@ function injectLoopAPI(targetWindow) { /** * 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, @@ -378,6 +380,9 @@ function injectLoopAPI(targetWindow) { * } * - {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 @@ -387,10 +392,9 @@ function injectLoopAPI(targetWindow) { 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 @@ -409,10 +413,9 @@ function injectLoopAPI(targetWindow) { LOOP_SESSION_TYPE: { enumerable: true, - writable: false, - value: function() { - return LOOP_SESSION_TYPE; - }, + get: function() { + return Cu.cloneInto(LOOP_SESSION_TYPE, targetWindow); + } }, logInToFxA: { @@ -450,11 +453,21 @@ function injectLoopAPI(targetWindow) { 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; } @@ -510,17 +523,23 @@ function injectLoopAPI(targetWindow) { 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); + } else { + // This isn't a window; but it should be a JS scope; used for testing + return targetWindow.mozLoop = contentObj; + } - // Handle window.close correctly on the panel and chatbox. - hookWindowCloseForPanelClose(targetWindow); } function getChromeWindow(contentWin) { diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index 5d4ba2e1d7e..4506bda6fc8 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -703,6 +703,11 @@ this.MozLoopService = { * 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")) { @@ -1052,4 +1057,3 @@ this.MozLoopService = { return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj); }, }; -Object.freeze(this.MozLoopService); diff --git a/browser/components/loop/content/js/client.js b/browser/components/loop/content/js/client.js index e3c6ebf2358..5bef639d635 100644 --- a/browser/components/loop/content/js/client.js +++ b/browser/components/loop/content/js/client.js @@ -108,29 +108,37 @@ loop.Client = (function($) { * @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; - } + 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; + } - try { - var urlData = JSON.parse(responseText); + try { + var urlData = JSON.parse(responseText); - // This throws if the data is invalid, in which case only the failure - // telementry will be recorded. - var returnData = this._validate(urlData, expectedCallUrlProperties); + // 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)); }, /** @@ -154,8 +162,7 @@ loop.Client = (function($) { }, _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; @@ -167,12 +174,19 @@ loop.Client = (function($) { 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. diff --git a/browser/components/loop/test/desktop-local/client_test.js b/browser/components/loop/test/desktop-local/client_test.js index e3876b70943..85d156ffe04 100644 --- a/browser/components/loop/test/desktop-local/client_test.js +++ b/browser/components/loop/test/desktop-local/client_test.js @@ -35,7 +35,12 @@ describe("loop.Client", function() { 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; @@ -70,6 +75,7 @@ describe("loop.Client", function() { sinon.assert.calledOnce(hawkRequestStub); sinon.assert.calledWith(hawkRequestStub, + mozLoop.LOOP_SESSION_TYPE.GUEST, "/call-url/" + fakeToken, "DELETE"); }); @@ -78,7 +84,7 @@ describe("loop.Client", 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); @@ -88,7 +94,7 @@ describe("loop.Client", function() { 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); @@ -119,8 +125,32 @@ describe("loop.Client", 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", @@ -132,8 +162,7 @@ describe("loop.Client", function() { // 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); @@ -149,8 +178,7 @@ describe("loop.Client", function() { // 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); @@ -166,7 +194,7 @@ describe("loop.Client", function() { // 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) { @@ -184,7 +212,7 @@ describe("loop.Client", function() { 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); @@ -197,7 +225,7 @@ describe("loop.Client", function() { 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); @@ -211,7 +239,7 @@ describe("loop.Client", function() { 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; diff --git a/browser/components/loop/test/xpcshell/test_loopapi_hawk_request.js b/browser/components/loop/test/xpcshell/test_loopapi_hawk_request.js new file mode 100644 index 00000000000..c94fd48ec1d --- /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(); +} diff --git a/browser/components/loop/test/xpcshell/xpcshell.ini b/browser/components/loop/test/xpcshell/xpcshell.ini index 39205063c01..d2b4625dadf 100644 --- a/browser/components/loop/test/xpcshell/xpcshell.ini +++ b/browser/components/loop/test/xpcshell/xpcshell.ini @@ -3,6 +3,7 @@ head = head.js tail = firefox-appdir = browser +[test_loopapi_hawk_request.js] [test_looppush_initialize.js] [test_loopservice_dnd.js] [test_loopservice_expiry.js]