diff options
Diffstat (limited to 'dom/push/test/test_utils.js')
-rw-r--r-- | dom/push/test/test_utils.js | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/dom/push/test/test_utils.js b/dom/push/test/test_utils.js new file mode 100644 index 0000000000..0214318d09 --- /dev/null +++ b/dom/push/test/test_utils.js @@ -0,0 +1,304 @@ +"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; +} |