Bug 1258883 - Add a way to replace the entire Push service in tests. r?wchen
MozReview-Commit-ID: ExJPShvXL5L
--- a/dom/push/PushComponents.js
+++ b/dom/push/PushComponents.js
@@ -11,16 +11,24 @@
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+// The default Push service implementation.
+XPCOMUtils.defineLazyGetter(this, "PushService", function() {
+ const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
+ {});
+ PushService.init();
+ return PushService;
+});
+
// Observer notification topics for system subscriptions. These are duplicated
// and used in `PushNotifier.cpp`. They're exposed on `nsIPushService` instead
// of `nsIPushNotifier` so that JS callers only need to import this service.
const OBSERVER_TOPIC_PUSH = "push-message";
const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
/**
* `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
@@ -94,24 +102,16 @@ function PushServiceParent() {
PushServiceBase.call(this);
}
PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
XPCOMUtils.defineLazyServiceGetter(PushServiceParent.prototype, "_mm",
"@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
-XPCOMUtils.defineLazyGetter(PushServiceParent.prototype, "_service",
- function() {
- const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
- {});
- PushService.init();
- return PushService;
-});
-
Object.assign(PushServiceParent.prototype, {
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceParent),
_messages: [
"Push:Register",
"Push:Registration",
"Push:Unregister",
"Push:Clear",
@@ -159,21 +159,21 @@ Object.assign(PushServiceParent.prototyp
}, error => {
callback.onClear(Cr.NS_ERROR_FAILURE);
}).catch(Cu.reportError);
},
// nsIPushQuotaManager methods
notificationForOriginShown(origin) {
- this._service.notificationForOriginShown(origin);
+ this.service.notificationForOriginShown(origin);
},
notificationForOriginClosed(origin) {
- this._service.notificationForOriginClosed(origin);
+ this.service.notificationForOriginClosed(origin);
},
receiveMessage(message) {
if (!this._isValidMessage(message)) {
return;
}
let {name, principal, target, data} = message;
if (name === "Push:NotificationForOriginShown") {
@@ -196,17 +196,17 @@ Object.assign(PushServiceParent.prototyp
}, error => {
sender.sendAsyncMessage(this._getResponseName(name, "KO"), {
requestID: data.requestID,
});
}).catch(Cu.reportError);
},
_handleReady() {
- this._service.init();
+ this.service.init();
},
_toPageRecord(principal, data) {
if (!data.scope) {
throw new Error("Invalid page record: missing scope");
}
if (!principal) {
throw new Error("Invalid page record: missing principal");
@@ -223,53 +223,63 @@ Object.assign(PushServiceParent.prototyp
data.originAttributes =
ChromeUtils.originAttributesToSuffix(principal.originAttributes);
return data;
},
_handleRequest(name, principal, data) {
if (name == "Push:Clear") {
- return this._service.clear(data);
+ return this.service.clear(data);
}
let pageRecord;
try {
pageRecord = this._toPageRecord(principal, data);
} catch (e) {
return Promise.reject(e);
}
if (name === "Push:Register") {
- return this._service.register(pageRecord);
+ return this.service.register(pageRecord);
}
if (name === "Push:Registration") {
- return this._service.registration(pageRecord);
+ return this.service.registration(pageRecord);
}
if (name === "Push:Unregister") {
- return this._service.unregister(pageRecord);
+ return this.service.unregister(pageRecord);
}
return Promise.reject(new Error("Invalid request: unknown name"));
},
_getResponseName(requestName, suffix) {
let name = requestName.slice("Push:".length);
return "PushService:" + name + ":" + suffix;
},
// Methods used for mocking in tests.
replaceServiceBackend(options) {
- this._service.changeTestServer(options.serverURI, options);
+ this.service.changeTestServer(options.serverURI, options);
},
restoreServiceBackend() {
var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
- this._service.changeTestServer(defaultServerURL);
+ this.service.changeTestServer(defaultServerURL);
+ },
+});
+
+// Used to replace the implementation with a mock.
+Object.defineProperty(PushServiceParent.prototype, "service", {
+ get() {
+ return this._service || PushService;
+ },
+ set(impl) {
+ this._service = impl;
},
});
/**
* The content process implementation of `nsIPushService`. This version
* uses the child message manager to forward calls to the parent process.
* The parent Push service instance handles the request, and responds with a
* message containing the result.
--- a/dom/push/test/mockpushserviceparent.js
+++ b/dom/push/test/mockpushserviceparent.js
@@ -108,8 +108,65 @@ addMessageListener("teardown", function
});
});
addMessageListener("server-msg", function (msg) {
mockWebSocket.then(socket => {
socket.serverSendMsg(msg);
});
});
+
+var MockService = {
+ requestID: 1,
+ resolvers: new Map(),
+
+ sendRequest(name, params) {
+ return new Promise((resolve, reject) => {
+ let id = this.requestID++;
+ this.resolvers.set(id, { resolve, reject });
+ sendAsyncMessage("service-request", {
+ name: name,
+ id: id,
+ params: params,
+ });
+ });
+ },
+
+ handleResponse(response) {
+ if (!this.resolvers.has(response.id)) {
+ Cu.reportError(`Unexpected response for request ${response.id}`);
+ return;
+ }
+ let resolver = this.resolvers.get(response.id);
+ this.resolvers.delete(response.id);
+ if (response.error) {
+ resolver.reject(response.error);
+ } else {
+ resolver.resolve(response.result);
+ }
+ },
+
+ init() {},
+
+ register(pageRecord) {
+ return this.sendRequest("register", pageRecord);
+ },
+
+ registration(pageRecord) {
+ return this.sendRequest("registration", pageRecord);
+ },
+
+ unregister(pageRecord) {
+ return this.sendRequest("unregister", pageRecord);
+ },
+};
+
+addMessageListener("replace-service", function () {
+ pushService.service = MockService;
+});
+
+addMessageListener("restore-service", function () {
+ pushService.service = null;
+});
+
+addMessageListener("service-response", function (response) {
+ MockService.handleResponse(response);
+});
--- a/dom/push/test/test_utils.js
+++ b/dom/push/test/test_utils.js
@@ -1,14 +1,42 @@
(function (g) {
"use strict";
let url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
let chromeScript = SpecialPowers.loadChromeScript(url);
+ function replacePushService(mockService) {
+ chromeScript.sendSyncMessage("replace-service");
+ chromeScript.addMessageListener("service-request", function(msg) {
+ let promise;
+ try {
+ let handler = mockService[msg.name];
+ promise = Promise.resolve(handler(msg.params));
+ } catch (error) {
+ promise = Promise.reject(error);
+ }
+ promise.then(result => {
+ chromeScript.sendAsyncMessage("service-response", {
+ id: msg.id,
+ result: result,
+ });
+ }, error => {
+ chromeScript.sendAsyncMessage("service-response", {
+ id: msg.id,
+ error: error,
+ });
+ });
+ });
+ }
+
+ function restorePushService() {
+ chromeScript.sendSyncMessage("restore-service");
+ }
+
let userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
let currentMockSocket = null;
function setupMockPushService(mockWebSocket) {
currentMockSocket = mockWebSocket;
currentMockSocket._isActive = true;
chromeScript.sendSyncMessage("setup");
@@ -93,50 +121,62 @@
chromeScript.sendAsyncMessage("server-msg", msg);
}
},
};
g.MockWebSocket = MockWebSocket;
g.setupMockPushService = setupMockPushService;
g.teardownMockPushService = teardownMockPushService;
+ g.replacePushService = replacePushService;
+ g.restorePushService = restorePushService;
}(this));
// Remove permissions and prefs when the test finishes.
SimpleTest.registerCleanupFunction(() => {
new Promise(resolve => {
SpecialPowers.flushPermissions(_ => {
SpecialPowers.flushPrefEnv(resolve);
});
}).then(_ => {
teardownMockPushService();
+ restorePushService();
});
});
function setPushPermission(allow) {
return new Promise(resolve => {
SpecialPowers.pushPermissions([
{ type: "desktop-notification", allow, context: document },
], resolve);
});
}
-function setupPrefsAndMock(mockSocket) {
+function setupPrefs() {
return new Promise(resolve => {
- setupMockPushService(mockSocket);
SpecialPowers.pushPrefEnv({"set": [
["dom.push.enabled", true],
["dom.push.connection.enabled", true],
["dom.serviceWorkers.exemptFromPerDomainMax", true],
["dom.serviceWorkers.enabled", true],
["dom.serviceWorkers.testing.enabled", true]
]}, resolve);
});
}
+function setupPrefsAndReplaceService(mockService) {
+ replacePushService(mockService);
+ return setupPrefs();
+}
+
+function setupPrefsAndMock(mockSocket) {
+ setupMockPushService(mockSocket);
+ return setupPrefs();
+}
+
function injectControlledFrame(target = document.body) {
return new Promise(function(res, rej) {
var iframe = document.createElement("iframe");
iframe.src = "/tests/dom/push/test/frame.html";
var controlledFrame = {
remove() {
target.removeChild(iframe);