Bug 1065153 - Support call URLs with guest or FxA hawk session types, r=MattN

This commit is contained in:
Dan Mosedale 2014-09-18 10:40:35 -07:00
parent bc8d2f36a9
commit e6b2d35f6f
7 changed files with 207 additions and 61 deletions

View File

@ -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");

View File

@ -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) {

View File

@ -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);

View File

@ -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.

View File

@ -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;

View File

@ -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();
}

View File

@ -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]