gecko-dev/dom/push/test/mockpushserviceparent.js
Kagami Sascha Rosylight 57a88e0d3d Bug 1847217 - Test permission before requesting for push r=edgar
This makes `pushManager.subscribe()` to succeed on GeckoView when permission is already granted, and allows basic permission testing on GeckoView test runner.

Differential Revision: https://phabricator.services.mozilla.com/D190961
2023-10-24 12:36:20 +00:00

208 lines
5.1 KiB
JavaScript

/* 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);
});