summaryrefslogtreecommitdiffstats
path: root/dom/push/test/mockpushserviceparent.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/test/mockpushserviceparent.js')
-rw-r--r--dom/push/test/mockpushserviceparent.js207
1 files changed, 207 insertions, 0 deletions
diff --git a/dom/push/test/mockpushserviceparent.js b/dom/push/test/mockpushserviceparent.js
new file mode 100644
index 0000000000..08d93f3aaf
--- /dev/null
+++ b/dom/push/test/mockpushserviceparent.js
@@ -0,0 +1,207 @@
+/* eslint-env mozilla/chrome-script */
+
+"use strict";
+
+/**
+ * Defers one or more callbacks until the next turn of the event loop. Multiple
+ * callbacks are executed in order.
+ *
+ * @param {Function[]} callbacks The callbacks to execute. One callback will be
+ * executed per tick.
+ */
+function waterfall(...callbacks) {
+ callbacks
+ .reduce(
+ (promise, callback) =>
+ promise.then(() => {
+ callback();
+ }),
+ Promise.resolve()
+ )
+ .catch(Cu.reportError);
+}
+
+/**
+ * Minimal implementation of a mock WebSocket connect to be used with
+ * PushService. Forwards and receive messages from the implementation
+ * that lives in the content process.
+ */
+function MockWebSocketParent(originalURI) {
+ this._originalURI = originalURI;
+}
+
+MockWebSocketParent.prototype = {
+ _originalURI: null,
+
+ _listener: null,
+ _context: null,
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWebSocketChannel"]),
+
+ get originalURI() {
+ return this._originalURI;
+ },
+
+ asyncOpen(uri, origin, originAttributes, windowId, listener, context) {
+ this._listener = listener;
+ this._context = context;
+ waterfall(() => this._listener.onStart(this._context));
+ },
+
+ sendMsg(msg) {
+ sendAsyncMessage("socket-client-msg", msg);
+ },
+
+ close() {
+ waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
+ },
+
+ serverSendMsg(msg) {
+ waterfall(
+ () => this._listener.onMessageAvailable(this._context, msg),
+ () => this._listener.onAcknowledge(this._context, 0)
+ );
+ },
+};
+
+var pushService = Cc["@mozilla.org/push/Service;1"].getService(
+ Ci.nsIPushService
+).wrappedJSObject;
+
+var mockSocket;
+var serverMsgs = [];
+
+addMessageListener("socket-setup", function () {
+ pushService.replaceServiceBackend({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ mockSocket = new MockWebSocketParent(uri);
+ while (serverMsgs.length) {
+ let msg = serverMsgs.shift();
+ mockSocket.serverSendMsg(msg);
+ }
+ return mockSocket;
+ },
+ });
+});
+
+addMessageListener("socket-teardown", function (msg) {
+ pushService
+ .restoreServiceBackend()
+ .then(_ => {
+ serverMsgs.length = 0;
+ if (mockSocket) {
+ mockSocket.close();
+ mockSocket = null;
+ }
+ sendAsyncMessage("socket-server-teardown");
+ })
+ .catch(error => {
+ Cu.reportError(`Error restoring service backend: ${error}`);
+ });
+});
+
+addMessageListener("socket-server-msg", function (msg) {
+ if (mockSocket) {
+ mockSocket.serverSendMsg(msg);
+ } else {
+ serverMsgs.push(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,
+ id,
+ // The request params from the real push service may contain a
+ // principal, which cannot be passed to the unprivileged
+ // mochitest scope, and will cause the message to be dropped if
+ // present. The mochitest scope fortunately does not need the
+ // principal, though, so set it to null before sending.
+ params: Object.assign({}, params, { principal: null }),
+ });
+ });
+ },
+
+ 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);
+ },
+
+ reportDeliveryError(messageId, reason) {
+ sendAsyncMessage("service-delivery-error", {
+ messageId,
+ reason,
+ });
+ },
+
+ uninit() {
+ return Promise.resolve();
+ },
+};
+
+async function replaceService(service) {
+ // `?.` because `service` can be null
+ // (either by calling this function with null, or the push module doesn't have the
+ // field at all e.g. in GeckoView)
+ // Passing null here resets it to the default implementation on desktop
+ // (so `.service` never becomes null there) but not for GeckoView.
+ // XXX(krosylight): we need to remove this deviation.
+ await pushService.service?.uninit();
+ pushService.service = service;
+ await pushService.service?.init();
+}
+
+addMessageListener("service-replace", function () {
+ replaceService(MockService)
+ .then(_ => {
+ sendAsyncMessage("service-replaced");
+ })
+ .catch(error => {
+ Cu.reportError(`Error replacing service: ${error}`);
+ });
+});
+
+addMessageListener("service-restore", function () {
+ replaceService(null)
+ .then(_ => {
+ sendAsyncMessage("service-restored");
+ })
+ .catch(error => {
+ Cu.reportError(`Error restoring service: ${error}`);
+ });
+});
+
+addMessageListener("service-response", function (response) {
+ MockService.handleResponse(response);
+});