Bug 1022689 Separate out the Loop Push Handler from the Loop Service code to help similify testing and clarify separation of modules. r=mhammond

This commit is contained in:
Mark Banner 2014-06-16 08:41:17 +01:00
parent dc9e6bfd0f
commit 70610d4899
12 changed files with 258 additions and 288 deletions

View File

@ -0,0 +1,154 @@
/* 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.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.EXPORTED_SYMBOLS = ["MozLoopPushHandler"];
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
/**
* We don't have push notifications on desktop currently, so this is a
* workaround to get them going for us.
*
* XXX Handle auto-reconnections if connection fails for whatever reason
* (bug 1013248).
*/
let MozLoopPushHandler = {
// This is the uri of the push server.
pushServerUri: Services.prefs.getCharPref("services.push.serverURL"),
// This is the channel id we're using for notifications
channelID: "8b1081ce-9b35-42b5-b8f5-3ff8cb813a50",
// Stores the push url if we're registered and we have one.
pushUrl: undefined,
/**
* Starts a connection to the push socket server. On
* connection, it will automatically say hello and register the channel
* id with the server.
*
* Register callback parameters:
* - {String|null} err: Encountered error, if any
* - {String} url: The push url obtained from the server
*
* Callback parameters:
* - {String} version The version string received from the push server for
* the notification.
*
* @param {Function} registerCallback Callback to be called once we are
* registered.
* @param {Function} notificationCallback Callback to be called when a
* push notification is received (may be called multiple
* times).
* @param {Object} mockPushHandler Optional, test-only object, to allow
* the websocket to be mocked for tests.
*/
initialize: function(registerCallback, notificationCallback, mockPushHandler) {
if (Services.io.offline) {
registerCallback("offline");
return;
}
this._registerCallback = registerCallback;
this._notificationCallback = notificationCallback;
if (mockPushHandler) {
// For tests, use the mock instance.
this._websocket = mockPushHandler;
} else {
this._websocket = Cc["@mozilla.org/network/protocol;1?name=wss"]
.createInstance(Ci.nsIWebSocketChannel);
}
this._websocket.protocol = "push-notification";
let pushURI = Services.io.newURI(this.pushServerUri, null, null);
this._websocket.asyncOpen(pushURI, this.pushServerUri, this, null);
},
/**
* Listener method, handles the start of the websocket stream.
* Sends a hello message to the server.
*
* @param {nsISupports} aContext Not used
*/
onStart: function() {
let helloMsg = { messageType: "hello", uaid: "", channelIDs: [] };
this._websocket.sendMsg(JSON.stringify(helloMsg));
},
/**
* Listener method, called when the websocket is closed.
*
* @param {nsISupports} aContext Not used
* @param {nsresult} aStatusCode Reason for stopping (NS_OK = successful)
*/
onStop: function(aContext, aStatusCode) {
// XXX We really should be handling auto-reconnect here, this will be
// implemented in bug 994151. For now, just log a warning, so that a
// developer can find out it has happened and not get too confused.
Cu.reportError("Loop Push server web socket closed! Code: " + aStatusCode);
this.pushUrl = undefined;
},
/**
* Listener method, called when the websocket is closed by the server.
* If there are errors, onStop may be called without ever calling this
* method.
*
* @param {nsISupports} aContext Not used
* @param {integer} aCode the websocket closing handshake close code
* @param {String} aReason the websocket closing handshake close reason
*/
onServerClose: function(aContext, aCode) {
// XXX We really should be handling auto-reconnect here, this will be
// implemented in bug 994151. For now, just log a warning, so that a
// developer can find out it has happened and not get too confused.
Cu.reportError("Loop Push server web socket closed (server)! Code: " + aCode);
this.pushUrl = undefined;
},
/**
* Listener method, called when the websocket receives a message.
*
* @param {nsISupports} aContext Not used
* @param {String} aMsg The message data
*/
onMessageAvailable: function(aContext, aMsg) {
let msg = JSON.parse(aMsg);
switch(msg.messageType) {
case "hello":
this._registerChannel();
break;
case "register":
this.pushUrl = msg.pushEndpoint;
this._registerCallback(null, this.pushUrl);
break;
case "notification":
msg.updates.forEach(function(update) {
if (update.channelID === this.channelID) {
this._notificationCallback(update.version);
}
}.bind(this));
break;
}
},
/**
* Handles registering a service
*/
_registerChannel: function() {
this._websocket.sendMsg(JSON.stringify({
messageType: "register",
channelID: this.channelID
}));
}
};

View File

@ -27,132 +27,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
XPCOMUtils.defineLazyModuleGetter(this, "HAWKAuthenticatedRESTRequest",
"resource://services-common/hawkrequest.js");
/**
* We don't have push notifications on desktop currently, so this is a
* workaround to get them going for us.
*
* XXX Handle auto-reconnections if connection fails for whatever reason
* (bug 1013248).
*/
let PushHandlerHack = {
// This is the uri of the push server.
pushServerUri: Services.prefs.getCharPref("services.push.serverURL"),
// This is the channel id we're using for notifications
channelID: "8b1081ce-9b35-42b5-b8f5-3ff8cb813a50",
// Stores the push url if we're registered and we have one.
pushUrl: undefined,
/**
* Call to start the connection to the push socket server. On
* connection, it will automatically say hello and register the channel
* id with the server.
*
* Register callback parameters:
* - {String|null} err: Encountered error, if any
* - {String} url: The push url obtained from the server
*
* @param {Function} registerCallback Callback to be called once we are
* registered.
* @param {Function} notificationCallback Callback to be called when a
* push notification is received.
*/
initialize: function(registerCallback, notificationCallback) {
if (Services.io.offline) {
registerCallback("offline");
return;
}
this._registerCallback = registerCallback;
this._notificationCallback = notificationCallback;
this.websocket = Cc["@mozilla.org/network/protocol;1?name=wss"]
.createInstance(Ci.nsIWebSocketChannel);
this.websocket.protocol = "push-notification";
var pushURI = Services.io.newURI(this.pushServerUri, null, null);
this.websocket.asyncOpen(pushURI, this.pushServerUri, this, null);
},
/**
* Listener method, handles the start of the websocket stream.
* Sends a hello message to the server.
*
* @param {nsISupports} aContext Not used
*/
onStart: function() {
var helloMsg = { messageType: "hello", uaid: "", channelIDs: [] };
this.websocket.sendMsg(JSON.stringify(helloMsg));
},
/**
* Listener method, called when the websocket is closed.
*
* @param {nsISupports} aContext Not used
* @param {nsresult} aStatusCode Reason for stopping (NS_OK = successful)
*/
onStop: function(aContext, aStatusCode) {
// XXX We really should be handling auto-reconnect here, this will be
// implemented in bug 994151. For now, just log a warning, so that a
// developer can find out it has happened and not get too confused.
Cu.reportError("Loop Push server web socket closed! Code: " + aStatusCode);
this.pushUrl = undefined;
},
/**
* Listener method, called when the websocket is closed by the server.
* If there are errors, onStop may be called without ever calling this
* method.
*
* @param {nsISupports} aContext Not used
* @param {integer} aCode the websocket closing handshake close code
* @param {String} aReason the websocket closing handshake close reason
*/
onServerClose: function(aContext, aCode) {
// XXX We really should be handling auto-reconnect here, this will be
// implemented in bug 994151. For now, just log a warning, so that a
// developer can find out it has happened and not get too confused.
Cu.reportError("Loop Push server web socket closed (server)! Code: " + aCode);
this.pushUrl = undefined;
},
/**
* Listener method, called when the websocket receives a message.
*
* @param {nsISupports} aContext Not used
* @param {String} aMsg The message data
*/
onMessageAvailable: function(aContext, aMsg) {
var msg = JSON.parse(aMsg);
switch(msg.messageType) {
case "hello":
this._registerChannel();
break;
case "register":
this.pushUrl = msg.pushEndpoint;
this._registerCallback(null, this.pushUrl);
break;
case "notification":
msg.updates.forEach(function(update) {
if (update.channelID === this.channelID) {
this._notificationCallback(update.version);
}
}.bind(this));
break;
}
},
/**
* Handles registering a service
*/
_registerChannel: function() {
this.websocket.sendMsg(JSON.stringify({
messageType: "register",
channelID: this.channelID
}));
}
};
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
"resource:///modules/loop/MozLoopPushHandler.jsm");
/**
* Internal helper methods and state
@ -238,10 +114,12 @@ let MozLoopServiceInternal = {
* Starts registration of Loop with the push server, and then will register
* with the Loop server. It will return early if already registered.
*
* @param {Object} mockPushHandler Optional, test-only mock push handler. Used
* to allow mocking of the MozLoopPushHandler.
* @returns {Promise} a promise that is resolved with no params on completion, or
* rejected with an error code or string.
*/
promiseRegisteredWithServers: function() {
promiseRegisteredWithServers: function(mockPushHandler) {
if (this._registeredDeferred) {
return this._registeredDeferred.promise;
}
@ -251,8 +129,10 @@ let MozLoopServiceInternal = {
// it back to null on error.
let result = this._registeredDeferred.promise;
PushHandlerHack.initialize(this.onPushRegistered.bind(this),
this.onHandleNotification.bind(this));
this._pushHandler = mockPushHandler || MozLoopPushHandler;
this._pushHandler.initialize(this.onPushRegistered.bind(this),
this.onHandleNotification.bind(this));
return result;
},
@ -282,7 +162,7 @@ let MozLoopServiceInternal = {
},
/**
* Callback from PushHandlerHack - The push server has been registered
* Callback from MozLoopPushHandler - The push server has been registered
* and has given us a push url.
*
* @param {String} pushUrl The push url given by the push server.
@ -340,7 +220,7 @@ let MozLoopServiceInternal = {
},
/**
* Callback from PushHandlerHack - A push notification has been received from
* Callback from MozLoopPushHandler - A push notification has been received from
* the server.
*
* @param {String} version The version information from the server.
@ -495,11 +375,13 @@ this.MozLoopService = {
* Starts registration of Loop with the push server, and then will register
* with the Loop server. It will return early if already registered.
*
* @param {Object} mockPushHandler Optional, test-only mock push handler. Used
* to allow mocking of the MozLoopPushHandler.
* @returns {Promise} a promise that is resolved with no params on completion, or
* rejected with an error code or string.
*/
register: function() {
return MozLoopServiceInternal.promiseRegisteredWithServers();
register: function(mockPushHandler) {
return MozLoopServiceInternal.promiseRegisteredWithServers(mockPushHandler);
},
/**

View File

@ -12,5 +12,6 @@ XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
EXTRA_JS_MODULES += [
'MozLoopAPI.jsm',
'MozLoopPushHandler.jsm',
'MozLoopService.jsm',
]

View File

@ -11,6 +11,9 @@ Cu.import("resource://testing-common/httpd.js");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService",
"resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
"resource:///modules/loop/MozLoopPushHandler.jsm");
const kMockWebSocketChannelName = "Mock WebSocket Channel";
const kWebSocketChannelContractID = "@mozilla.org/network/protocol;1?name=wss";
@ -33,6 +36,32 @@ function setupFakeLoopServer() {
});
}
/**
* This is used to fake push registration and notifications for
* MozLoopService tests. There is only one object created per test instance, as
* once registration has taken place, the object cannot currently be changed.
*/
let mockPushHandler = {
// This sets the registration result to be returned when initialize
// is called. By default, it is equivalent to success.
registrationResult: null,
/**
* MozLoopPushHandler API
*/
initialize: function(registerCallback, notificationCallback) {
registerCallback(this.registrationResult);
this._notificationCallback = notificationCallback;
},
/**
* Test-only API to simplify notifying a push notification result.
*/
notify: function(version) {
this._notificationCallback(version);
}
};
/**
* Mock nsIWebSocketChannel for tests. This mocks the WebSocketChannel, and
* enables us to check parameters and return messages similar to the push
@ -82,80 +111,3 @@ MockWebSocketChannel.prototype = {
}
}
};
/**
* The XPCOM factory for registering and creating the mock.
*/
let gMockWebSocketChannelFactory = {
_registered: false,
// We keep a list of instances so that we can access them outside of the service
// that creates them.
createdInstances: [],
resetInstances: function() {
this.createdInstances = [];
},
get CID() {
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
return registrar.contractIDToCID(kWebSocketChannelContractID);
},
/**
* Registers the MockWebSocketChannel, and stores the original.
*/
register: function() {
if (this._registered)
return;
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
this._origFactory = Components.manager
.getClassObject(Cc[kWebSocketChannelContractID],
Ci.nsIFactory);
registrar.unregisterFactory(Components.ID(this.CID),
this._origFactory);
registrar.registerFactory(Components.ID(this.CID),
kMockWebSocketChannelName,
kWebSocketChannelContractID,
gMockWebSocketChannelFactory);
this._registered = true;
},
/* Unregisters the MockWebSocketChannel, and re-registers the original
* Prompt Service.
*/
unregister: function() {
if (!this._registered)
return;
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
registrar.unregisterFactory(Components.ID(this.CID),
gMockWebSocketChannelFactory);
registrar.registerFactory(Components.ID(this.CID),
kMockWebSocketChannelName,
kWebSocketChannelContractID,
this._origFactory);
delete this._origFactory;
this._registered = false;
},
createInstance: function(outer, iid) {
if (outer != null) {
throw Cr.NS_ERROR_NO_AGGREGATION;
}
let newChannel = new MockWebSocketChannel()
this.createdInstances.push(newChannel);
return newChannel;
}
};

View File

@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
add_test(function test_initalize_offline() {
Services.io.offline = true;
MozLoopPushHandler.initialize(function(err) {
Assert.equal(err, "offline", "Should error with 'offline' when offline");
Services.io.offline = false;
run_next_test();
});
});
add_test(function test_initalize_websocket() {
let mockWebSocket = new MockWebSocketChannel();
MozLoopPushHandler.initialize(function(err) {
Assert.equal(err, null, "Should return null for success");
Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
"Should have the url from preferences");
Assert.equal(mockWebSocket.origin, kServerPushUrl,
"Should have the origin url from preferences");
Assert.equal(mockWebSocket.protocol, "push-notification",
"Should have the protocol set to push-notifications");
run_next_test();
}, function() {}, mockWebSocket);
});
function run_test() {
Services.prefs.setCharPref("services.push.serverURL", kServerPushUrl);
run_next_test();
};

View File

@ -5,7 +5,7 @@
XPCOMUtils.defineLazyModuleGetter(this, "Chat",
"resource:///modules/Chat.jsm");
var openChatOrig = Chat.open;
let openChatOrig = Chat.open;
add_test(function test_get_do_not_disturb() {
Services.prefs.setBoolPref("loop.do_not_disturb", false);
@ -32,15 +32,13 @@ add_test(function test_set_do_not_disturb() {
add_test(function test_do_not_disturb_disabled_should_open_chat_window() {
MozLoopService.doNotDisturb = false;
MozLoopService.register().then(() => {
let webSocket = gMockWebSocketChannelFactory.createdInstances[0];
MozLoopService.register(mockPushHandler).then(() => {
let opened = false;
Chat.open = function() {
opened = true;
};
webSocket.notify(1);
mockPushHandler.notify(1);
do_check_true(opened, "should open a chat window");
@ -48,23 +46,18 @@ add_test(function test_do_not_disturb_disabled_should_open_chat_window() {
});
});
add_test(function test_do_not_disturb_enabled_shouldnt_open_chat_window() {
add_task(function test_do_not_disturb_enabled_shouldnt_open_chat_window() {
MozLoopService.doNotDisturb = true;
MozLoopService.register().then(() => {
let webSocket = gMockWebSocketChannelFactory.createdInstances[0];
// We registered in the previous test, so no need to do that on this one.
let opened = false;
Chat.open = function() {
opened = true;
};
let opened = false;
Chat.open = function() {
opened = true;
};
mockPushHandler.notify(1);
webSocket.notify(1);
do_check_false(opened, "should not open a chat window");
run_next_test();
});
do_check_false(opened, "should not open a chat window");
});
function run_test()
@ -77,12 +70,7 @@ function run_test()
response.finish();
});
// Registrations and pref settings.
gMockWebSocketChannelFactory.register();
do_register_cleanup(function() {
gMockWebSocketChannelFactory.unregister();
// Revert original Chat.open implementation
Chat.open = openChatOrig;

View File

@ -50,26 +50,11 @@ add_task(function test_initialize_starts_timer() {
function run_test()
{
setupFakeLoopServer();
loopServer.registerPathHandler("/registration", (request, response) => {
response.setStatusLine(null, 200, "OK");
response.processAsync();
response.finish();
});
// Registrations and pref settings.
gMockWebSocketChannelFactory.register();
// Override MozLoopService's initializeTimer, so that we can verify the timeout is called
// correctly.
MozLoopService._startInitializeTimer = function() {
startTimerCalled = true;
};
do_register_cleanup(function() {
gMockWebSocketChannelFactory.unregister();
});
run_next_test();
}

View File

@ -12,10 +12,12 @@
* Tests reported failures when we're in offline mode.
*/
add_test(function test_register_offline() {
mockPushHandler.registrationResult = "offline";
// It should callback with failure if in offline mode
Services.io.offline = true;
MozLoopService.register().then(() => {
MozLoopService.register(mockPushHandler).then(() => {
do_throw("should not succeed when offline");
}, err => {
Assert.equal(err, "offline", "should reject with 'offline' when offline");
@ -29,25 +31,15 @@ add_test(function test_register_offline() {
* failure is reported.
*/
add_test(function test_register_websocket_success_loop_server_fail() {
MozLoopService.register().then(() => {
mockPushHandler.registrationResult = null;
MozLoopService.register(mockPushHandler).then(() => {
do_throw("should not succeed when loop server registration fails");
}, err => {
// 404 is an expected failure indicated by the lack of route being set
// up on the Loop server mock. This is added in the next test.
Assert.equal(err, 404, "Expected no errors in websocket registration");
let instances = gMockWebSocketChannelFactory.createdInstances;
Assert.equal(instances.length, 1,
"Should create only one instance of websocket");
Assert.equal(instances[0].uri.prePath, kServerPushUrl,
"Should have the url from preferences");
Assert.equal(instances[0].origin, kServerPushUrl,
"Should have the origin url from preferences");
Assert.equal(instances[0].protocol, "push-notification",
"Should have the protocol set to push-notifications");
gMockWebSocketChannelFactory.resetInstances();
run_next_test();
});
});
@ -62,14 +54,10 @@ add_test(function test_register_success() {
response.processAsync();
response.finish();
});
MozLoopService.register().then(() => {
let instances = gMockWebSocketChannelFactory.createdInstances;
Assert.equal(instances.length, 1,
"Should create only one instance of websocket");
MozLoopService.register(mockPushHandler).then(() => {
run_next_test();
}, err => {
do_throw("shouldn't error on a succesful request");
do_throw("shouldn't error on a successful request");
});
});
@ -77,11 +65,7 @@ function run_test()
{
setupFakeLoopServer();
// Registrations and pref settings.
gMockWebSocketChannelFactory.register();
do_register_cleanup(function() {
gMockWebSocketChannelFactory.unregister();
Services.prefs.clearUserPref("loop.hawk-session-token");
});

View File

@ -6,7 +6,6 @@
* header is returned with the registration response.
*/
add_test(function test_registration_returns_hawk_session_token() {
var fakeSessionToken = "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1750";
Services.prefs.clearUserPref("loop.hawk-session-token");
@ -17,7 +16,7 @@ add_test(function test_registration_returns_hawk_session_token() {
response.finish();
});
MozLoopService.register().then(() => {
MozLoopService.register(mockPushHandler).then(() => {
var hawkSessionPref;
try {
hawkSessionPref = Services.prefs.getCharPref("loop.hawk-session-token");
@ -36,11 +35,7 @@ function run_test()
{
setupFakeLoopServer();
// Registrations and pref settings.
gMockWebSocketChannelFactory.register();
do_register_cleanup(function() {
gMockWebSocketChannelFactory.unregister();
Services.prefs.clearUserPref("loop.hawk-session-token");
});

View File

@ -24,7 +24,7 @@ add_test(function test_registration_uses_hawk_session_token() {
response.finish();
});
MozLoopService.register().then(() => {
MozLoopService.register(mockPushHandler).then(() => {
run_next_test();
}, err => {
do_throw("shouldn't error on a succesful request");
@ -36,11 +36,7 @@ function run_test()
{
setupFakeLoopServer();
// Registrations and pref settings.
gMockWebSocketChannelFactory.register();
do_register_cleanup(function() {
gMockWebSocketChannelFactory.unregister();
Services.prefs.clearUserPref("loop.hawk-session-token");
});

View File

@ -16,7 +16,7 @@ add_test(function test_registration_handles_bogus_hawk_token() {
response.finish();
});
MozLoopService.register().then(() => {
MozLoopService.register(mockPushHandler).then(() => {
do_throw("should not succeed with a bogus token");
}, err => {
@ -40,11 +40,7 @@ function run_test()
{
setupFakeLoopServer();
// Registrations and pref settings.
gMockWebSocketChannelFactory.register();
do_register_cleanup(function() {
gMockWebSocketChannelFactory.unregister();
Services.prefs.clearUserPref("loop.hawk-session-token");
});

View File

@ -3,6 +3,7 @@ head = head.js
tail =
firefox-appdir = browser
[test_looppush_initialize.js]
[test_loopservice_dnd.js]
[test_loopservice_expiry.js]
[test_loopservice_get_loop_char_pref.js]