2019-04-09 10:05:51 +00:00
|
|
|
"use strict";
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
const url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
|
|
|
|
const chromeScript = SpecialPowers.loadChromeScript(url);
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
/**
|
|
|
|
* 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) {
|
2023-05-20 12:26:49 +00:00
|
|
|
chromeScript.addMessageListener("service-delivery-error", function (msg) {
|
2019-04-09 10:05:51 +00:00
|
|
|
mockService.reportDeliveryError(msg.messageId, msg.reason);
|
|
|
|
});
|
2023-05-20 12:26:49 +00:00
|
|
|
chromeScript.addMessageListener("service-request", function (msg) {
|
2019-04-09 10:05:51 +00:00
|
|
|
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,
|
2016-03-23 00:34:41 +00:00
|
|
|
});
|
2019-04-09 10:05:51 +00:00
|
|
|
},
|
|
|
|
error => {
|
|
|
|
chromeScript.sendAsyncMessage("service-response", {
|
|
|
|
id: msg.id,
|
|
|
|
error,
|
2019-03-14 22:37:51 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
2019-04-09 10:05:51 +00:00
|
|
|
});
|
|
|
|
await new Promise(resolve => {
|
|
|
|
chromeScript.addMessageListener("service-replaced", function onReplaced() {
|
|
|
|
chromeScript.removeMessageListener("service-replaced", onReplaced);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
chromeScript.sendAsyncMessage("service-replace");
|
|
|
|
});
|
|
|
|
}
|
2016-03-23 00:34:41 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
async function restorePushService() {
|
|
|
|
await new Promise(resolve => {
|
|
|
|
chromeScript.addMessageListener("service-restored", function onRestored() {
|
|
|
|
chromeScript.removeMessageListener("service-restored", onRestored);
|
|
|
|
resolve();
|
2019-03-14 22:37:51 +00:00
|
|
|
});
|
2019-04-09 10:05:51 +00:00
|
|
|
chromeScript.sendAsyncMessage("service-restore");
|
|
|
|
});
|
|
|
|
}
|
2016-03-23 00:34:41 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
let currentMockSocket = null;
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
Bug 1541557: Part 5 - Update callers of ChromeScript.sendSyncMessage to use sendQuery instead. r=nika
Since JSWindowActors don't have direct access to synchronous messaging,
ChromeScript callers are going to need to migrate to asynchronous messaging
and queries instead.
Since there's no comparable API to sendQuery for frame message managers, this
patch adds a stub that uses synchronous messaging, but makes the API appear
asynchronous, and migrates callers to use it instead of direct synchronous
messaging. This will be replaced with a true synchronous API in the actor
migration.
Fortunately, most of the time, this actually leads to simpler code. The
`sendQuery` API doesn't have the odd return value semantics of
`sendSyncMessage`, and can usually just be used as a drop-in replacement. Many
of the `sendSyncMessage` callers don't actually use the result, and can just
be changed to `sendAsyncMessage`. And many of the existing async messaging
users can be changed to just use `sendQuery` rather than sending messages and
adding response listeners.
However, the APZ code is an exception. It relies on intricate properties of
the event loop, and doesn't have an easy way to slot in promise handlers, so I
migrated it to using sync messaging via process message managers instead.
Differential Revision: https://phabricator.services.mozilla.com/D35055
--HG--
extra : rebase_source : d5707e87f293a831a5cf2e0b0a7e977090267f78
extra : source : 75ebd6fce136ab3bd0e591c2b8b2d06d3b5bf923
2019-06-12 19:40:51 +00:00
|
|
|
chromeScript.sendAsyncMessage("socket-setup");
|
2023-05-20 12:26:49 +00:00
|
|
|
chromeScript.addMessageListener("socket-client-msg", function (msg) {
|
2019-04-09 10:05:51 +00:00
|
|
|
mockWebSocket.handleMessage(msg);
|
|
|
|
});
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
function teardownMockPushSocket() {
|
|
|
|
if (currentMockSocket) {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
currentMockSocket._isActive = false;
|
|
|
|
chromeScript.addMessageListener("socket-server-teardown", resolve);
|
Bug 1541557: Part 5 - Update callers of ChromeScript.sendSyncMessage to use sendQuery instead. r=nika
Since JSWindowActors don't have direct access to synchronous messaging,
ChromeScript callers are going to need to migrate to asynchronous messaging
and queries instead.
Since there's no comparable API to sendQuery for frame message managers, this
patch adds a stub that uses synchronous messaging, but makes the API appear
asynchronous, and migrates callers to use it instead of direct synchronous
messaging. This will be replaced with a true synchronous API in the actor
migration.
Fortunately, most of the time, this actually leads to simpler code. The
`sendQuery` API doesn't have the odd return value semantics of
`sendSyncMessage`, and can usually just be used as a drop-in replacement. Many
of the `sendSyncMessage` callers don't actually use the result, and can just
be changed to `sendAsyncMessage`. And many of the existing async messaging
users can be changed to just use `sendQuery` rather than sending messages and
adding response listeners.
However, the APZ code is an exception. It relies on intricate properties of
the event loop, and doesn't have an easy way to slot in promise handlers, so I
migrated it to using sync messaging via process message managers instead.
Differential Revision: https://phabricator.services.mozilla.com/D35055
--HG--
extra : rebase_source : d5707e87f293a831a5cf2e0b0a7e977090267f78
extra : source : 75ebd6fce136ab3bd0e591c2b8b2d06d3b5bf923
2019-06-12 19:40:51 +00:00
|
|
|
chromeScript.sendAsyncMessage("socket-teardown");
|
2016-03-18 00:11:22 +00:00
|
|
|
});
|
|
|
|
}
|
2019-04-09 10:05:51 +00:00
|
|
|
return Promise.resolve();
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
/**
|
|
|
|
* 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 {
|
2016-03-18 00:11:22 +00:00
|
|
|
// Default implementation to make the push server work minimally.
|
|
|
|
// Override methods to implement custom functionality.
|
2019-04-09 10:05:51 +00:00
|
|
|
constructor() {
|
|
|
|
this.userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
|
|
|
|
this.registerCount = 0;
|
2016-03-18 00:11:22 +00:00
|
|
|
// 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.
|
2019-04-09 10:05:51 +00:00
|
|
|
this._isActive = false;
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
onHello(request) {
|
|
|
|
this.serverSendMsg(
|
|
|
|
JSON.stringify({
|
|
|
|
messageType: "hello",
|
|
|
|
uaid: this.userAgentID,
|
|
|
|
status: 200,
|
|
|
|
use_webpush: true,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
onRegister(request) {
|
|
|
|
this.serverSendMsg(
|
|
|
|
JSON.stringify({
|
|
|
|
messageType: "register",
|
|
|
|
uaid: this.userAgentID,
|
|
|
|
channelID: request.channelID,
|
|
|
|
status: 200,
|
|
|
|
pushEndpoint: "https://example.com/endpoint/" + this.registerCount++,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
onUnregister(request) {
|
|
|
|
this.serverSendMsg(
|
|
|
|
JSON.stringify({
|
|
|
|
messageType: "unregister",
|
|
|
|
channelID: request.channelID,
|
|
|
|
status: 200,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
onAck(request) {
|
|
|
|
// Do nothing.
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2019-04-09 10:05:51 +00:00
|
|
|
serverSendMsg(msg) {
|
|
|
|
if (this._isActive) {
|
|
|
|
chromeScript.sendAsyncMessage("socket-server-msg", msg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-03-18 00:11:22 +00:00
|
|
|
|
2016-01-28 15:57:42 +00:00
|
|
|
// Remove permissions and prefs when the test finishes.
|
2023-05-20 12:26:49 +00:00
|
|
|
SimpleTest.registerCleanupFunction(async function () {
|
2019-03-14 22:37:51 +00:00
|
|
|
await new Promise(resolve => SpecialPowers.flushPermissions(resolve));
|
|
|
|
await SpecialPowers.flushPrefEnv();
|
|
|
|
await restorePushService();
|
|
|
|
await teardownMockPushSocket();
|
2016-03-18 00:11:22 +00:00
|
|
|
});
|
2016-01-28 15:57:42 +00:00
|
|
|
|
|
|
|
function setPushPermission(allow) {
|
2022-01-31 13:51:27 +00:00
|
|
|
let permissions = [
|
2019-10-27 15:28:41 +00:00
|
|
|
{ type: "desktop-notification", allow, context: document },
|
2022-01-31 13:51:27 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
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.
|
2023-05-20 12:26:53 +00:00
|
|
|
let partitionedPrincipal =
|
|
|
|
SpecialPowers.wrap(document).partitionedPrincipal;
|
2022-01-31 13:51:27 +00:00
|
|
|
|
|
|
|
permissions.push({
|
|
|
|
type: "desktop-notification",
|
|
|
|
allow,
|
|
|
|
context: {
|
|
|
|
url: partitionedPrincipal.originNoSuffix,
|
|
|
|
originAttributes: {
|
|
|
|
partitionKey: partitionedPrincipal.originAttributes.partitionKey,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return SpecialPowers.pushPermissions(permissions);
|
2016-01-28 15:57:42 +00:00
|
|
|
}
|
|
|
|
|
2016-03-23 00:34:41 +00:00
|
|
|
function setupPrefs() {
|
2016-05-17 15:32:05 +00:00
|
|
|
return SpecialPowers.pushPrefEnv({
|
|
|
|
set: [
|
|
|
|
["dom.push.enabled", true],
|
|
|
|
["dom.push.connection.enabled", true],
|
2016-06-04 20:17:12 +00:00
|
|
|
["dom.push.maxRecentMessageIDsPerSubscription", 0],
|
2016-05-17 15:32:05 +00:00
|
|
|
["dom.serviceWorkers.exemptFromPerDomainMax", true],
|
|
|
|
["dom.serviceWorkers.enabled", true],
|
2019-04-09 10:05:51 +00:00
|
|
|
["dom.serviceWorkers.testing.enabled", true],
|
2016-05-17 15:32:05 +00:00
|
|
|
],
|
|
|
|
});
|
2016-01-28 15:57:42 +00:00
|
|
|
}
|
|
|
|
|
2019-03-14 22:37:51 +00:00
|
|
|
async function setupPrefsAndReplaceService(mockService) {
|
|
|
|
await replacePushService(mockService);
|
|
|
|
await setupPrefs();
|
2016-03-23 00:34:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function setupPrefsAndMockSocket(mockSocket) {
|
|
|
|
setupMockPushSocket(mockSocket);
|
|
|
|
return setupPrefs();
|
|
|
|
}
|
|
|
|
|
2016-01-28 15:57:42 +00:00
|
|
|
function injectControlledFrame(target = document.body) {
|
2023-05-20 12:26:49 +00:00
|
|
|
return new Promise(function (res, rej) {
|
2016-01-28 15:57:42 +00:00
|
|
|
var iframe = document.createElement("iframe");
|
2016-01-29 03:26:17 +00:00
|
|
|
iframe.src = "/tests/dom/push/test/frame.html";
|
2016-01-28 15:57:42 +00:00
|
|
|
|
|
|
|
var controlledFrame = {
|
|
|
|
remove() {
|
|
|
|
target.removeChild(iframe);
|
|
|
|
iframe = null;
|
|
|
|
},
|
2016-01-29 03:26:17 +00:00
|
|
|
waitOnWorkerMessage(type) {
|
|
|
|
return iframe
|
|
|
|
? iframe.contentWindow.waitOnWorkerMessage(type)
|
2016-01-28 15:57:42 +00:00
|
|
|
: Promise.reject(new Error("Frame removed from document"));
|
|
|
|
},
|
2016-03-28 20:33:20 +00:00
|
|
|
innerWindowId() {
|
2020-08-17 20:20:50 +00:00
|
|
|
return SpecialPowers.wrap(iframe).browsingContext.currentWindowContext
|
|
|
|
.innerWindowId;
|
2016-03-28 20:33:20 +00:00
|
|
|
},
|
2016-01-28 15:57:42 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2016-08-18 14:12:09 +00:00
|
|
|
|
|
|
|
function waitForActive(swr) {
|
|
|
|
let sw = swr.installing || swr.waiting || swr.active;
|
|
|
|
return new Promise(resolve => {
|
2019-04-09 10:05:51 +00:00
|
|
|
if (sw.state === "activated") {
|
2016-08-18 14:12:09 +00:00
|
|
|
resolve(swr);
|
|
|
|
return;
|
|
|
|
}
|
2019-04-09 10:05:51 +00:00
|
|
|
sw.addEventListener("statechange", function onStateChange(evt) {
|
|
|
|
if (sw.state === "activated") {
|
|
|
|
sw.removeEventListener("statechange", onStateChange);
|
2016-08-18 14:12:09 +00:00
|
|
|
resolve(swr);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2017-02-07 21:56:01 +00:00
|
|
|
|
|
|
|
function base64UrlDecode(s) {
|
2019-04-09 10:05:51 +00:00
|
|
|
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
2017-02-07 21:56:01 +00:00
|
|
|
|
|
|
|
// 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:
|
2019-04-09 10:05:51 +00:00
|
|
|
s += "==";
|
2017-02-07 21:56:01 +00:00
|
|
|
break; // Two pad chars
|
|
|
|
case 3:
|
2019-04-09 10:05:51 +00:00
|
|
|
s += "=";
|
2017-02-07 21:56:01 +00:00
|
|
|
break; // One pad char
|
|
|
|
default:
|
2019-04-09 10:05:51 +00:00
|
|
|
throw new Error("Illegal base64url string!");
|
2017-02-07 21:56:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|