mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
130a655906
Differential Revision: https://phabricator.services.mozilla.com/D177027
305 lines
8.3 KiB
JavaScript
305 lines
8.3 KiB
JavaScript
"use strict";
|
|
|
|
const url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
|
|
const chromeScript = SpecialPowers.loadChromeScript(url);
|
|
|
|
/**
|
|
* Replaces `PushService.jsm` with a mock implementation that handles requests
|
|
* from the DOM API. This allows tests to simulate local errors and error
|
|
* reporting, bypassing the `PushService.jsm` machinery.
|
|
*/
|
|
async function replacePushService(mockService) {
|
|
chromeScript.addMessageListener("service-delivery-error", function (msg) {
|
|
mockService.reportDeliveryError(msg.messageId, msg.reason);
|
|
});
|
|
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,
|
|
});
|
|
},
|
|
error => {
|
|
chromeScript.sendAsyncMessage("service-response", {
|
|
id: msg.id,
|
|
error,
|
|
});
|
|
}
|
|
);
|
|
});
|
|
await new Promise(resolve => {
|
|
chromeScript.addMessageListener("service-replaced", function onReplaced() {
|
|
chromeScript.removeMessageListener("service-replaced", onReplaced);
|
|
resolve();
|
|
});
|
|
chromeScript.sendAsyncMessage("service-replace");
|
|
});
|
|
}
|
|
|
|
async function restorePushService() {
|
|
await new Promise(resolve => {
|
|
chromeScript.addMessageListener("service-restored", function onRestored() {
|
|
chromeScript.removeMessageListener("service-restored", onRestored);
|
|
resolve();
|
|
});
|
|
chromeScript.sendAsyncMessage("service-restore");
|
|
});
|
|
}
|
|
|
|
let currentMockSocket = null;
|
|
|
|
/**
|
|
* Sets up a mock connection for the WebSocket backend. This only replaces
|
|
* the transport layer; `PushService.jsm` still handles DOM API requests,
|
|
* observes permission changes, writes to IndexedDB, and notifies service
|
|
* workers of incoming push messages.
|
|
*/
|
|
function setupMockPushSocket(mockWebSocket) {
|
|
currentMockSocket = mockWebSocket;
|
|
currentMockSocket._isActive = true;
|
|
chromeScript.sendAsyncMessage("socket-setup");
|
|
chromeScript.addMessageListener("socket-client-msg", function (msg) {
|
|
mockWebSocket.handleMessage(msg);
|
|
});
|
|
}
|
|
|
|
function teardownMockPushSocket() {
|
|
if (currentMockSocket) {
|
|
return new Promise(resolve => {
|
|
currentMockSocket._isActive = false;
|
|
chromeScript.addMessageListener("socket-server-teardown", resolve);
|
|
chromeScript.sendAsyncMessage("socket-teardown");
|
|
});
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* Minimal implementation of web sockets for use in testing. Forwards
|
|
* messages to a mock web socket in the parent process that is used
|
|
* by the push service.
|
|
*/
|
|
class MockWebSocket {
|
|
// Default implementation to make the push server work minimally.
|
|
// Override methods to implement custom functionality.
|
|
constructor() {
|
|
this.userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
|
|
this.registerCount = 0;
|
|
// We only allow one active mock web socket to talk to the parent.
|
|
// This flag is used to keep track of which mock web socket is active.
|
|
this._isActive = false;
|
|
}
|
|
|
|
onHello(request) {
|
|
this.serverSendMsg(
|
|
JSON.stringify({
|
|
messageType: "hello",
|
|
uaid: this.userAgentID,
|
|
status: 200,
|
|
use_webpush: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
onRegister(request) {
|
|
this.serverSendMsg(
|
|
JSON.stringify({
|
|
messageType: "register",
|
|
uaid: this.userAgentID,
|
|
channelID: request.channelID,
|
|
status: 200,
|
|
pushEndpoint: "https://example.com/endpoint/" + this.registerCount++,
|
|
})
|
|
);
|
|
}
|
|
|
|
onUnregister(request) {
|
|
this.serverSendMsg(
|
|
JSON.stringify({
|
|
messageType: "unregister",
|
|
channelID: request.channelID,
|
|
status: 200,
|
|
})
|
|
);
|
|
}
|
|
|
|
onAck(request) {
|
|
// Do nothing.
|
|
}
|
|
|
|
handleMessage(msg) {
|
|
let request = JSON.parse(msg);
|
|
let messageType = request.messageType;
|
|
switch (messageType) {
|
|
case "hello":
|
|
this.onHello(request);
|
|
break;
|
|
case "register":
|
|
this.onRegister(request);
|
|
break;
|
|
case "unregister":
|
|
this.onUnregister(request);
|
|
break;
|
|
case "ack":
|
|
this.onAck(request);
|
|
break;
|
|
default:
|
|
throw new Error("Unexpected message: " + messageType);
|
|
}
|
|
}
|
|
|
|
serverSendMsg(msg) {
|
|
if (this._isActive) {
|
|
chromeScript.sendAsyncMessage("socket-server-msg", msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove permissions and prefs when the test finishes.
|
|
SimpleTest.registerCleanupFunction(async function () {
|
|
await new Promise(resolve => SpecialPowers.flushPermissions(resolve));
|
|
await SpecialPowers.flushPrefEnv();
|
|
await restorePushService();
|
|
await teardownMockPushSocket();
|
|
});
|
|
|
|
function setPushPermission(allow) {
|
|
let permissions = [
|
|
{ type: "desktop-notification", allow, context: document },
|
|
];
|
|
|
|
if (isXOrigin) {
|
|
// We need to add permission for the xorigin tests. In xorigin tests, the
|
|
// test page will be run under third-party context, so we need to use
|
|
// partitioned principal to add the permission.
|
|
let partitionedPrincipal =
|
|
SpecialPowers.wrap(document).partitionedPrincipal;
|
|
|
|
permissions.push({
|
|
type: "desktop-notification",
|
|
allow,
|
|
context: {
|
|
url: partitionedPrincipal.originNoSuffix,
|
|
originAttributes: {
|
|
partitionKey: partitionedPrincipal.originAttributes.partitionKey,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
return SpecialPowers.pushPermissions(permissions);
|
|
}
|
|
|
|
function setupPrefs() {
|
|
return SpecialPowers.pushPrefEnv({
|
|
set: [
|
|
["dom.push.enabled", true],
|
|
["dom.push.connection.enabled", true],
|
|
["dom.push.maxRecentMessageIDsPerSubscription", 0],
|
|
["dom.serviceWorkers.exemptFromPerDomainMax", true],
|
|
["dom.serviceWorkers.enabled", true],
|
|
["dom.serviceWorkers.testing.enabled", true],
|
|
],
|
|
});
|
|
}
|
|
|
|
async function setupPrefsAndReplaceService(mockService) {
|
|
await replacePushService(mockService);
|
|
await setupPrefs();
|
|
}
|
|
|
|
function setupPrefsAndMockSocket(mockSocket) {
|
|
setupMockPushSocket(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);
|
|
iframe = null;
|
|
},
|
|
waitOnWorkerMessage(type) {
|
|
return iframe
|
|
? iframe.contentWindow.waitOnWorkerMessage(type)
|
|
: Promise.reject(new Error("Frame removed from document"));
|
|
},
|
|
innerWindowId() {
|
|
return SpecialPowers.wrap(iframe).browsingContext.currentWindowContext
|
|
.innerWindowId;
|
|
},
|
|
};
|
|
|
|
iframe.onload = () => res(controlledFrame);
|
|
target.appendChild(iframe);
|
|
});
|
|
}
|
|
|
|
function sendRequestToWorker(request) {
|
|
return navigator.serviceWorker.ready.then(registration => {
|
|
return new Promise((resolve, reject) => {
|
|
var channel = new MessageChannel();
|
|
channel.port1.onmessage = e => {
|
|
(e.data.error ? reject : resolve)(e.data);
|
|
};
|
|
registration.active.postMessage(request, [channel.port2]);
|
|
});
|
|
});
|
|
}
|
|
|
|
function waitForActive(swr) {
|
|
let sw = swr.installing || swr.waiting || swr.active;
|
|
return new Promise(resolve => {
|
|
if (sw.state === "activated") {
|
|
resolve(swr);
|
|
return;
|
|
}
|
|
sw.addEventListener("statechange", function onStateChange(evt) {
|
|
if (sw.state === "activated") {
|
|
sw.removeEventListener("statechange", onStateChange);
|
|
resolve(swr);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function base64UrlDecode(s) {
|
|
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
|
|
// Replace padding if it was stripped by the sender.
|
|
// See http://tools.ietf.org/html/rfc4648#section-4
|
|
switch (s.length % 4) {
|
|
case 0:
|
|
break; // No pad chars in this case
|
|
case 2:
|
|
s += "==";
|
|
break; // Two pad chars
|
|
case 3:
|
|
s += "=";
|
|
break; // One pad char
|
|
default:
|
|
throw new Error("Illegal base64url string!");
|
|
}
|
|
|
|
// With correct padding restored, apply the standard base64 decoder
|
|
var decoded = atob(s);
|
|
|
|
var array = new Uint8Array(new ArrayBuffer(decoded.length));
|
|
for (var i = 0; i < decoded.length; i++) {
|
|
array[i] = decoded.charCodeAt(i);
|
|
}
|
|
return array;
|
|
}
|