diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/workers/test/xpcshell | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/workers/test/xpcshell')
-rw-r--r-- | dom/workers/test/xpcshell/data/base_uri_module.mjs | 23 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/data/base_uri_module2.mjs | 1 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/data/base_uri_worker.js | 27 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/data/chrome.manifest | 1 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/data/worker.js | 6 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/data/worker_fileReader.js | 7 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js | 558 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js | 112 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/test_fileReader.js | 33 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/test_import_base_uri.js | 35 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js | 88 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/test_workers.js | 37 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/test_workers_clone_error.js | 43 | ||||
-rw-r--r-- | dom/workers/test/xpcshell/xpcshell.toml | 38 |
14 files changed, 1009 insertions, 0 deletions
diff --git a/dom/workers/test/xpcshell/data/base_uri_module.mjs b/dom/workers/test/xpcshell/data/base_uri_module.mjs new file mode 100644 index 0000000000..7604baed82 --- /dev/null +++ b/dom/workers/test/xpcshell/data/base_uri_module.mjs @@ -0,0 +1,23 @@ +// This file is for testing the module loader's path handling. +// ESLint rules that modifies path shouldn't be applied. + +export const obj = {}; + +export async function doImport() { + // This file is loaded as resource://test/data/base_uri_module.mjs + // Relative/absolute paths should be resolved based on the URI, instead of + // file: path. + + const namespaceWithURI = await import( + "resource://test/data/base_uri_module2.mjs" + ); + const namespaceWithCurrentDir = await import("./base_uri_module2.mjs"); + const namespaceWithParentDir = await import("../data/base_uri_module2.mjs"); + const namespaceWithAbsoluteDir = await import("/data/base_uri_module2.mjs"); + + return { + equal1: namespaceWithURI.obj2 == namespaceWithCurrentDir.obj2, + equal2: namespaceWithURI.obj2 == namespaceWithParentDir.obj2, + equal3: namespaceWithURI.obj2 == namespaceWithAbsoluteDir.obj2, + }; +} diff --git a/dom/workers/test/xpcshell/data/base_uri_module2.mjs b/dom/workers/test/xpcshell/data/base_uri_module2.mjs new file mode 100644 index 0000000000..2358d27a83 --- /dev/null +++ b/dom/workers/test/xpcshell/data/base_uri_module2.mjs @@ -0,0 +1 @@ +export const obj2 = {}; diff --git a/dom/workers/test/xpcshell/data/base_uri_worker.js b/dom/workers/test/xpcshell/data/base_uri_worker.js new file mode 100644 index 0000000000..74137cc20b --- /dev/null +++ b/dom/workers/test/xpcshell/data/base_uri_worker.js @@ -0,0 +1,27 @@ +// This file is for testing the module loader's path handling. +// ESLint rules that modifies path shouldn't be applied. + +onmessage = async event => { + // This file is loaded as resource://test/data/base_uri_worker.js + // Relative/absolute paths should be resolved based on the URI, instead of + // file: path. + + const namespaceWithURI = await import( + "resource://test/data/base_uri_module.mjs" + ); + const namespaceWithCurrentDir = await import("./base_uri_module.mjs"); + const namespaceWithParentDir = await import("../data/base_uri_module.mjs"); + const namespaceWithAbsoluteDir = await import("/data/base_uri_module.mjs"); + + postMessage({ + scriptToModule: { + equal1: namespaceWithURI.obj == namespaceWithCurrentDir.obj, + equal2: namespaceWithURI.obj == namespaceWithParentDir.obj, + equal3: namespaceWithURI.obj == namespaceWithAbsoluteDir.obj, + }, + moduleToModuleURI: await namespaceWithURI.doImport(), + moduleToModuleCurrent: await namespaceWithCurrentDir.doImport(), + moduleToModuleParent: await namespaceWithParentDir.doImport(), + moduleToModuleAbsolute: await namespaceWithAbsoluteDir.doImport(), + }); +}; diff --git a/dom/workers/test/xpcshell/data/chrome.manifest b/dom/workers/test/xpcshell/data/chrome.manifest new file mode 100644 index 0000000000..611e81fd4e --- /dev/null +++ b/dom/workers/test/xpcshell/data/chrome.manifest @@ -0,0 +1 @@ +content workers ./ diff --git a/dom/workers/test/xpcshell/data/worker.js b/dom/workers/test/xpcshell/data/worker.js new file mode 100644 index 0000000000..0a455f51c3 --- /dev/null +++ b/dom/workers/test/xpcshell/data/worker.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +self.onmessage = function (msg) { + self.postMessage("OK"); +}; diff --git a/dom/workers/test/xpcshell/data/worker_fileReader.js b/dom/workers/test/xpcshell/data/worker_fileReader.js new file mode 100644 index 0000000000..44e7e6499b --- /dev/null +++ b/dom/workers/test/xpcshell/data/worker_fileReader.js @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +self.onmessage = function (msg) { + var fr = new FileReader(); + self.postMessage("OK"); +}; diff --git a/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js b/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js new file mode 100644 index 0000000000..3028e5d539 --- /dev/null +++ b/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js @@ -0,0 +1,558 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { createHttpServer } = AddonTestUtils; + +// Force ServiceWorkerRegistrar to init by calling do_get_profile. +// (This has to be called before AddonTestUtils.init, because it does +// also call do_get_profile internally but it doesn't notify +// profile-after-change). +do_get_profile(true); + +AddonTestUtils.init(this); +ExtensionTestUtils.init(this); + +const server = createHttpServer({ hosts: ["localhost"] }); + +server.registerPathHandler("/page.html", (request, response) => { + info(`/page.html is being requested: ${JSON.stringify(request)}`); + response.write(`<!DOCTYPE html>`); +}); + +server.registerPathHandler("/sw.js", (request, response) => { + info(`/sw.js is being requested: ${JSON.stringify(request)}`); + response.setHeader("Content-Type", "application/javascript"); + response.write(` + dump('Executing http://localhost/sw.js\\n'); + importScripts('sw-imported.js'); + dump('Executed importScripts from http://localhost/sw.js\\n'); + `); +}); + +server.registerPathHandler("/sw-imported.js", (request, response) => { + info(`/sw-imported.js is being requested: ${JSON.stringify(request)}`); + response.setHeader("Content-Type", "application/javascript"); + response.write(` + dump('importScript loaded from http://localhost/sw-imported.js\\n'); + self.onmessage = evt => evt.ports[0].postMessage('original-imported-script'); + `); +}); + +Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true); +// Make sure this test file doesn't run with the legacy behavior by +// setting explicitly the expected default value. +Services.prefs.setBoolPref( + "extensions.filterResponseServiceWorkerScript.disabled", + false +); +Services.prefs.setBoolPref("extensions.dnr.enabled", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled"); + Services.prefs.clearUserPref( + "extensions.filterResponseServiceWorkerScript.disabled" + ); + Services.prefs.clearUserPref("extensions.dnr.enabled"); +}); + +// Helper function used to be sure to clear any data that a previous test case +// may have left (e.g. service worker registration, cached service worker +// scripts). +// +// NOTE: Given that xpcshell test are running isolated from each other (unlike +// mochitests), we can just clear every storage type supported by clear data +// (instead of cherry picking what we want to clear based on the test cases +// part of this test file). +async function ensureDataCleanup() { + info("Clear any service worker or data previous test cases may have left"); + await new Promise(resolve => + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve) + ); +} + +// Note that the update algorithm (https://w3c.github.io/ServiceWorker/#update-algorithm) +// builds an "updatedResourceMap" as part of its check process. This means that only a +// single fetch will be performed for "sw-imported.js" as part of the update check and its +// resulting install invocation. The installation's call to importScripts when evaluated +// will load the script directly out of the Cache API. +function testSWUpdate(contentPage) { + return contentPage.spawn([], async () => { + const oldReg = await this.content.navigator.serviceWorker.ready; + const reg = await oldReg.update(); + const sw = reg.installing || reg.waiting || reg.active; + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + sw.postMessage("worker-message", [port2]); + }); + }); +} + +add_task(async function test_extension_invalid_sw_scripts_redirect_ignored() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["<all_urls>", "webRequest", "webRequestBlocking"], + // In this test task, the extension resource is not expected to be + // requested at all, so it does not really matter whether the file is + // listed in web_accessible_resources. Regardless, add it to make sure + // that any load failure is not caused by the lack of being listed here. + web_accessible_resources: ["sw-unexpected-redirect.js"], + }, + background() { + browser.webRequest.onBeforeRequest.addListener( + req => { + if (req.url == "http://localhost/sw.js") { + const filter = browser.webRequest.filterResponseData(req.requestId); + filter.ondata = event => filter.write(event.data); + filter.onstop = event => filter.disconnect(); + filter.onerror = () => { + browser.test.sendMessage( + "filter-response-error:mainscript", + filter.error + ); + }; + return { + redirectUrl: browser.runtime.getURL("sw-unexpected-redirect.js"), + }; + } + + if (req.url == "http://localhost/sw-imported.js") { + const filter = browser.webRequest.filterResponseData(req.requestId); + filter.ondata = event => filter.write(event.data); + filter.onstop = event => filter.disconnect(); + filter.onerror = () => { + browser.test.sendMessage( + "filter-response-error:importscript", + filter.error + ); + }; + return { redirectUrl: "about:blank" }; + } + + return {}; + }, + { urls: ["http://localhost/sw.js", "http://localhost/sw-imported.js"] }, + ["blocking"] + ); + }, + files: { + "sw-unexpected-redirect.js": ` + dump('main worker redirected to moz-extension://UUID/sw-unexpected-redirect.js\\n'); + self.onmessage = evt => evt.ports[0].postMessage('sw-unexpected-redirect'); + `, + }, + }); + + // Start the test extension to redirect importScripts requests. + await extension.startup(); + + function awaitConsoleMessage(regexp) { + return new Promise(resolve => { + Services.console.registerListener(function listener(message) { + if (regexp.test(message.message)) { + Services.console.unregisterListener(listener); + resolve(message); + } + }); + }); + } + + const awaitIgnoredMainScriptRedirect = awaitConsoleMessage( + /Invalid redirectUrl .* on service worker main script/ + ); + const awaitIgnoredImportScriptRedirect = awaitConsoleMessage( + /Invalid redirectUrl .* on service worker imported script/ + ); + + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://localhost/page.html" + ); + + // Register the worker after loading the test extension, which should not be + // able to intercept and redirect the importedScripts requests because of + // invalid destinations. + info("Register service worker from a content webpage"); + let workerMessage = await contentPage.spawn([], async () => { + const reg = await this.content.navigator.serviceWorker.register("/sw.js"); + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + const sw = reg.active || reg.waiting || reg.installing; + sw.postMessage("worker-message", [port2]); + }); + }); + + equal( + workerMessage, + "original-imported-script", + "Got expected worker reply (importScripts not intercepted)" + ); + + info("Wait for the expected error message on main script redirect"); + const errorMsg = await awaitIgnoredMainScriptRedirect; + ok(errorMsg?.message, `Got error message: ${errorMsg?.message}`); + ok( + errorMsg?.message?.includes(extension.id), + "error message should include the addon id" + ); + ok( + errorMsg?.message?.includes("http://localhost/sw.js"), + "error message should include the sw main script url" + ); + + info("Wait for the expected error message on import script redirect"); + const errorMsg2 = await awaitIgnoredImportScriptRedirect; + ok(errorMsg2?.message, `Got error message: ${errorMsg2?.message}`); + ok( + errorMsg2?.message?.includes(extension.id), + "error message should include the addon id" + ); + ok( + errorMsg2?.message?.includes("http://localhost/sw-imported.js"), + "error message should include the sw main script url" + ); + + info("Wait filterResponse error on main script"); + equal( + await extension.awaitMessage("filter-response-error:mainscript"), + "Invalid request ID", + "Got expected error on main script" + ); + info("Wait filterResponse error on import script"); + equal( + await extension.awaitMessage("filter-response-error:importscript"), + "Invalid request ID", + "Got expected error on import script" + ); + + await extension.unload(); + await contentPage.close(); +}); + +add_task(async function test_filter_sw_script() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: [ + "<all_urls>", + "webRequest", + "webRequestBlocking", + "webRequestFilterResponse.serviceWorkerScript", + ], + }, + background() { + browser.webRequest.onBeforeRequest.addListener( + req => { + if (req.url == "http://localhost/sw.js") { + const filter = browser.webRequest.filterResponseData(req.requestId); + let decoder = new TextDecoder("utf-8"); + let encoder = new TextEncoder(); + filter.ondata = event => { + let str = decoder.decode(event.data, { stream: true }); + browser.test.log(`Got filter ondata event: ${str}\n`); + str = ` + dump('Executing filterResponse script for http://localhost/sw.js\\n'); + self.onmessage = evt => evt.ports[0].postMessage('filter-response-script'); + dump('Executed firlterResponse script for http://localhost/sw.js\\n'); + `; + filter.write(encoder.encode(str)); + filter.disconnect(); + }; + } + + return {}; + }, + { urls: ["http://localhost/sw.js", "http://localhost/sw-imported.js"] }, + ["blocking"] + ); + }, + }); + + // Start the test extension to redirect importScripts requests. + await extension.startup(); + + await ensureDataCleanup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://localhost/page.html" + ); + + let workerMessage = await contentPage.spawn([], async () => { + const reg = await this.content.navigator.serviceWorker.register("/sw.js"); + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + const sw = reg.active || reg.waiting || reg.installing; + sw.postMessage("worker-message", [port2]); + }); + }); + + equal( + workerMessage, + "filter-response-script", + "Got expected worker reply (filterResponse script)" + ); + + await extension.unload(); + workerMessage = await testSWUpdate(contentPage); + equal( + workerMessage, + "original-imported-script", + "Got expected worker reply (original script)" + ); + + await contentPage.close(); +}); + +add_task(async function test_extension_redirect_sw_imported_script() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["<all_urls>", "webRequest", "webRequestBlocking"], + web_accessible_resources: ["sw-imported-1.js", "sw-imported-2.js"], + }, + background() { + let i = 1; + browser.webRequest.onBeforeRequest.addListener( + req => { + browser.test.log( + "Extension is redirecting http://localhost/sw-imported.js" + ); + browser.test.sendMessage("request-redirected"); + return { + redirectUrl: browser.runtime.getURL(`sw-imported-${i++}.js`), + }; + }, + { urls: ["http://localhost/sw-imported.js"] }, + ["blocking"] + ); + }, + files: { + "sw-imported-1.js": ` + dump('importScript redirected to moz-extension://UUID/sw-imported1.js \\n'); + self.onmessage = evt => evt.ports[0].postMessage('redirected-imported-script-1'); + `, + "sw-imported-2.js": ` + dump('importScript redirected to moz-extension://UUID/sw-imported2.js \\n'); + self.onmessage = evt => evt.ports[0].postMessage('redirected-imported-script-2'); + `, + }, + }); + + await ensureDataCleanup(); + const contentPage = await ExtensionTestUtils.loadContentPage( + "http://localhost/page.html" + ); + + // Register the worker while the test extension isn't loaded and cannot + // intercept and redirect the importedScripts requests. + let workerMessage = await contentPage.spawn([], async () => { + const reg = await this.content.navigator.serviceWorker.register("/sw.js"); + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + const sw = reg.active || reg.waiting || reg.installing; + sw.postMessage("worker-message", [port2]); + }); + }); + + equal( + workerMessage, + "original-imported-script", + "Got expected worker reply (importScripts not intercepted)" + ); + + // Start the test extension to redirect importScripts requests. + await extension.startup(); + + // Trigger an update on the registered service worker, then assert that the + // reply got is coming from the script where the extension is redirecting the + // request. + info("Update service worker and expect extension script to reply"); + workerMessage = await testSWUpdate(contentPage); + await extension.awaitMessage("request-redirected"); + equal( + workerMessage, + "redirected-imported-script-1", + "Got expected worker reply (importScripts redirected to moz-extension url)" + ); + + // Trigger a new update of the registered service worker, then assert that the + // reply got is coming from a different script where the extension is + // redirecting the second request (this confirms that the extension can + // intercept and can change the redirected imported scripts on new service + // worker updates). + info("Update service worker and expect new extension script to reply"); + workerMessage = await testSWUpdate(contentPage); + await extension.awaitMessage("request-redirected"); + equal( + workerMessage, + "redirected-imported-script-2", + "Got expected worker reply (importScripts redirected to moz-extension url again)" + ); + + // Uninstall the extension and trigger one more update of the registered + // service worker, then assert that the reply got is the one coming from the + // server (because difference from the one got from the cache). + // This verify that the service worker are updated as expected after the + // extension is uninstalled and the worker is not stuck on the script where + // the extension did redirect the request the last time. + info( + "Unload extension, update service worker and expect original script to reply" + ); + await extension.unload(); + workerMessage = await testSWUpdate(contentPage); + equal( + workerMessage, + "original-imported-script", + "Got expected worker reply (importScripts not intercepted)" + ); + + await contentPage.close(); +}); + +// Test cases for redirects with declarativeNetRequest instead of webRequest, +// testing the same scenarios as: +// - test_extension_invalid_sw_scripts_redirect_ignored +// i.e. fail to redirect SW main script, fail to redirect SW to about:blank. +// - test_extension_redirect_sw_imported_script +// i.e. allowed to redirect importScripts to moz-extension. +add_task(async function test_dnr_redirect_sw_script_or_import() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + manifest_version: 3, + permissions: ["declarativeNetRequest"], + host_permissions: ["<all_urls>"], + granted_host_permissions: true, + web_accessible_resources: [ + { + resources: ["sw-bad-redirect.js", "sw-dnr-redirect.js", "sw-nest.js"], + matches: ["*://*/*"], + }, + ], + }, + temporarilyInstalled: true, // <-- for granted_host_permissions + background: async () => { + await browser.declarativeNetRequest.updateSessionRules({ + addRules: [ + { + id: 1, + condition: { urlFilter: "|http://localhost/sw.js?dnr_redir_bad" }, + action: { + type: "redirect", + redirect: { extensionPath: "/sw-bad-redirect.js" }, + }, + }, + { + id: 2, + condition: { urlFilter: "|http://localhost/sw-imported.js|" }, + action: { + type: "redirect", + redirect: { extensionPath: "/sw-dnr-redirect.js" }, + }, + }, + { + id: 3, + condition: { urlFilter: "|http://localhost/sw-nest.js|" }, + action: { + type: "redirect", + redirect: { extensionPath: "/sw-nest.js" }, + }, + }, + { + id: 4, + condition: { urlFilter: "|http://localhost/sw-imported.js?about|" }, + action: { + type: "redirect", + redirect: { url: "about:blank" }, + }, + }, + ], + }); + browser.test.sendMessage("dnr_registered"); + }, + files: { + "sw-bad-redirect.js": String.raw` + dump('main worker redirected to moz-extension://UUID/sw-bad-redirect.js\n'); + self.onmessage = evt => evt.ports[0].postMessage('sw-bad-redirect'); + `, + "sw-dnr-redirect.js": String.raw` + dump('importScript redirected to moz-extension://UUID/sw-dnr-redirect.js\n'); + self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-before-nest'); + + importScripts("/sw-nest.js"); + // ^ sw-nest.js does not exist on the server, so if importScripts() + // succeeded, then that means that the DNR-triggered redirect worked. + + self.onmessage = evt => evt.ports[0].postMessage('sw-before-about'); + try { + importScripts("/sw-imported.js?about"); + // ^ DNR redirects to about:blank, which should throw here. + self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-about-bad'); + } catch (e) { + // All is good. + self.onmessage = evt => evt.ports[0].postMessage('sw-dnr-redirect'); + } + `, + "sw-nest.js": String.raw` + dump('importScript redirected to moz-extension://UUID/sw-nest.js\n'); + // No other code here. The caller verifies success by confirming that + // the importScripts() call did not throw. + `, + }, + }); + + // Start the test extension to redirect importScripts requests. + await extension.startup(); + await extension.awaitMessage("dnr_registered"); + + await ensureDataCleanup(); + let contentPage = await ExtensionTestUtils.loadContentPage( + "http://localhost/page.html" + ); + + // Register the worker after loading the test extension, which should not be + // able to intercept and redirect the importedScripts requests because of + // invalid destinations. + info("Register service worker from a content webpage (disallowed redirects)"); + await contentPage.spawn([], async () => { + await Assert.rejects( + this.content.navigator.serviceWorker.register("/sw.js?dnr_redir_bad1"), + /SecurityError: The operation is insecure/, + "Redirect of main service worker script is not allowed" + ); + }); + info("Register service worker from a content webpage (with import redirect)"); + let workerMessage = await contentPage.spawn([], async () => { + const reg = await this.content.navigator.serviceWorker.register("/sw.js"); + return new Promise(resolve => { + const { MessageChannel } = this.content; + const { port1, port2 } = new MessageChannel(); + port1.onmessage = evt => resolve(evt.data); + const sw = reg.active || reg.waiting || reg.installing; + sw.postMessage("worker-message", [port2]); + }); + }); + + equal( + workerMessage, + "sw-dnr-redirect", + "Got expected worker reply (importScripts redirected to moz-extension:-URL)" + ); + + await extension.unload(); + await contentPage.close(); +}); diff --git a/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js b/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js new file mode 100644 index 0000000000..718093422b --- /dev/null +++ b/dom/workers/test/xpcshell/test_ext_worker_offline_fetch.js @@ -0,0 +1,112 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +const { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +const { createHttpServer } = AddonTestUtils; + +// Force ServiceWorkerRegistrar to init by calling do_get_profile. +// (This has to be called before AddonTestUtils.init, because it does +// also call do_get_profile internally but it doesn't notify +// profile-after-change). +do_get_profile(true); + +AddonTestUtils.init(this); +ExtensionTestUtils.init(this); + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler("/dummy", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + response.write("dummy page"); +}); + +add_setup(() => { + info("Making sure Services.io.offline is true"); + // Explicitly setting Services.io.offline to true makes this test able + // to hit on Desktop builds the same issue that test_ext_cache_api.js + // was hitting on Android builds (Bug 1844825). + Services.io.offline = true; +}); + +// Regression test derived from Bug 1844825. +add_task(async function test_fetch_request_from_ext_shared_worker() { + if (!WebExtensionPolicy.useRemoteWebExtensions) { + // Ensure RemoteWorkerService has been initialized in the main + // process. + Services.obs.notifyObservers(null, "profile-after-change"); + } + + const background = async function () { + const testUrl = `http://example.com/dummy`; + const worker = new SharedWorker("worker.js"); + const { data: result } = await new Promise(resolve => { + worker.port.onmessage = resolve; + worker.port.postMessage(["worker-fetch-test", testUrl]); + }); + + browser.test.sendMessage("test-sharedworker-fetch:done", result); + }; + + const extension = ExtensionTestUtils.loadExtension({ + background, + manifest: { permissions: ["http://example.com/*"] }, + files: { + "worker.js": function () { + self.onconnect = evt => { + const port = evt.ports[0]; + port.onmessage = async evt => { + let result = {}; + let message; + try { + const [msg, url] = evt.data; + message = msg; + const response = await fetch(url); + dump(`fetch call resolved: ${response}\n`); + result.fetchResolvesTo = `${response}`; + } catch (err) { + dump(`fetch call rejected: ${err}\n`); + result.error = err.name; + throw err; + } finally { + port.postMessage([`${message}:result`, result]); + } + }; + }; + }, + }, + }); + + await extension.startup(); + const result = await extension.awaitMessage("test-sharedworker-fetch:done"); + if (Services.io.offline && WebExtensionPolicy.useRemoteWebExtensions) { + // If the network is offline and the extensions are running in the + // child extension process, expect the fetch call to be rejected + // with an TypeError. + Assert.deepEqual( + ["worker-fetch-test:result", { error: "TypeError" }], + result, + "fetch should have been rejected with an TypeError" + ); + } else { + // If the network is not offline or the extension are running in the + // parent process, we expect the fetch call to resolve to a Response. + Assert.deepEqual( + ["worker-fetch-test:result", { fetchResolvesTo: "[object Response]" }], + result, + "fetch should have been resolved to a Response instance" + ); + } + await extension.unload(); +}); diff --git a/dom/workers/test/xpcshell/test_fileReader.js b/dom/workers/test/xpcshell/test_fileReader.js new file mode 100644 index 0000000000..02bc3fa667 --- /dev/null +++ b/dom/workers/test/xpcshell/test_fileReader.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Worker must be loaded from a chrome:// uri, not a file:// +// uri, so we first need to load it. +var WORKER_SOURCE_URI = "chrome://workers/content/worker_fileReader.js"; +do_load_manifest("data/chrome.manifest"); + +function talk_with_worker(worker) { + return new Promise((resolve, reject) => { + worker.onmessage = function (event) { + let success = true; + if (event.data == "OK") { + resolve(); + } else { + success = false; + reject(event); + } + Assert.ok(success); + worker.terminate(); + }; + worker.onerror = function (event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + reject(error); + }; + worker.postMessage("START"); + }); +} + +add_task(function test_chrome_worker() { + return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI)); +}); diff --git a/dom/workers/test/xpcshell/test_import_base_uri.js b/dom/workers/test/xpcshell/test_import_base_uri.js new file mode 100644 index 0000000000..7c88a946f4 --- /dev/null +++ b/dom/workers/test/xpcshell/test_import_base_uri.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function testSyncImportBeforeAsyncImportDependencyInWorker() { + const worker = new ChromeWorker("resource://test/data/base_uri_worker.js"); + + const { promise, resolve } = Promise.withResolvers(); + worker.onmessage = event => { + resolve(event.data); + }; + worker.postMessage(""); + + const result = await promise; + + Assert.ok(result.scriptToModule.equal1); + Assert.ok(result.scriptToModule.equal2); + Assert.ok(result.scriptToModule.equal3); + + Assert.ok(result.moduleToModuleURI.equal1); + Assert.ok(result.moduleToModuleURI.equal2); + Assert.ok(result.moduleToModuleURI.equal3); + + Assert.ok(result.moduleToModuleCurrent.equal1); + Assert.ok(result.moduleToModuleCurrent.equal2); + Assert.ok(result.moduleToModuleCurrent.equal3); + + Assert.ok(result.moduleToModuleParent.equal1); + Assert.ok(result.moduleToModuleParent.equal2); + Assert.ok(result.moduleToModuleParent.equal3); + + Assert.ok(result.moduleToModuleAbsolute.equal1); + Assert.ok(result.moduleToModuleAbsolute.equal2); + Assert.ok(result.moduleToModuleAbsolute.equal3); +}); diff --git a/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js b/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js new file mode 100644 index 0000000000..b95ad4bcaf --- /dev/null +++ b/dom/workers/test/xpcshell/test_remoteworker_launch_new_process.js @@ -0,0 +1,88 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); +const { createHttpServer } = AddonTestUtils; + +// Force ServiceWorkerRegistrar to init by calling do_get_profile. +// (This has to be called before AddonTestUtils.init, because it does +// also call do_get_profile internally but it doesn't notify +// profile-after-change). +do_get_profile(true); + +AddonTestUtils.init(this); + +const server = createHttpServer({ hosts: ["localhost"] }); + +server.registerPathHandler("/sw.js", (request, response) => { + info(`/sw.js is being requested: ${JSON.stringify(request)}`); + response.setHeader("Content-Type", "application/javascript"); + response.write(""); +}); + +add_task(async function setup_prefs() { + // Enable nsIServiceWorkerManager.registerForTest. + Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled"); + }); +}); + +/** + * This test installs a ServiceWorker via test API and verify that the install + * process spawns a new process. (Normally ServiceWorker installation won't + * cause a new content process to be spawned because the call to register must + * be coming from within an existing content process, but the registerForTest + * API allows us to bypass this restriction.) + * + * This models the real-world situation of a push notification being received + * from the network which results in a ServiceWorker being spawned without their + * necessarily being an existing content process to host it (especially under Fission). + */ +add_task(async function launch_remoteworkers_in_new_processes() { + const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + + const ssm = Services.scriptSecurityManager; + + const initialChildCount = Services.ppmm.childCount; + + // A test service worker that should spawn a regular web content child process. + const swRegInfoWeb = await swm.registerForTest( + ssm.createContentPrincipal(Services.io.newURI("http://localhost"), {}), + "http://localhost/scope", + "http://localhost/sw.js" + ); + swRegInfoWeb.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo); + + info( + `web content service worker registered: ${JSON.stringify({ + principal: swRegInfoWeb.principal.spec, + scope: swRegInfoWeb.scope, + })}` + ); + + info("Wait new process to be launched"); + await TestUtils.waitForCondition(() => { + return Services.ppmm.childCount - initialChildCount >= 1; + }, "wait for a new child processes to be started"); + + // Wait both workers to become active to be sure that. besides spawning + // the new child processes as expected, the two remote worker have been + // able to run successfully (in other word their remote worker data did + // pass successfull the IsRemoteTypeAllowed check in RemoteworkerChild). + info("Wait for webcontent worker to become active"); + await TestUtils.waitForCondition( + () => swRegInfoWeb.activeWorker, + `wait workers for scope ${swRegInfoWeb.scope} to be active` + ); +}); diff --git a/dom/workers/test/xpcshell/test_workers.js b/dom/workers/test/xpcshell/test_workers.js new file mode 100644 index 0000000000..5b768f69bb --- /dev/null +++ b/dom/workers/test/xpcshell/test_workers.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Worker must be loaded from a chrome:// uri, not a file:// +// uri, so we first need to load it. +var WORKER_SOURCE_URI = "chrome://workers/content/worker.js"; +do_load_manifest("data/chrome.manifest"); + +function talk_with_worker(worker) { + return new Promise((resolve, reject) => { + worker.onmessage = function (event) { + let success = true; + if (event.data == "OK") { + resolve(); + } else { + success = false; + reject(event); + } + Assert.ok(success); + worker.terminate(); + }; + worker.onerror = function (event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + reject(error); + }; + worker.postMessage("START"); + }); +} + +add_task(function test_chrome_worker() { + return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI)); +}); + +add_task(function test_worker() { + return talk_with_worker(new Worker(WORKER_SOURCE_URI)); +}); diff --git a/dom/workers/test/xpcshell/test_workers_clone_error.js b/dom/workers/test/xpcshell/test_workers_clone_error.js new file mode 100644 index 0000000000..f3fd430457 --- /dev/null +++ b/dom/workers/test/xpcshell/test_workers_clone_error.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Worker must be loaded from a chrome:// uri, not a file:// +// uri, so we first need to load it. +var WORKER_SOURCE_URI = "chrome://workers/content/worker.js"; +do_load_manifest("data/chrome.manifest"); + +function talk_with_worker(worker) { + return new Promise((resolve, reject) => { + worker.onmessage = function (event) { + let success = true; + if (event.data == "OK") { + resolve(); + } else { + success = false; + reject(event); + } + Assert.ok(success); + worker.terminate(); + }; + worker.onerror = function (event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + reject(error); + }; + + try { + // eslint-disable-next-line no-eval + eval("/"); + } catch (e) { + worker.postMessage(new ClonedErrorHolder(e)); + } + }); +} + +add_task(function test_chrome_worker() { + return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI)); +}); + +add_task(function test_worker() { + return talk_with_worker(new Worker(WORKER_SOURCE_URI)); +}); diff --git a/dom/workers/test/xpcshell/xpcshell.toml b/dom/workers/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..61b1eef0c2 --- /dev/null +++ b/dom/workers/test/xpcshell/xpcshell.toml @@ -0,0 +1,38 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = [ + "data/worker.js", + "data/worker_fileReader.js", + "data/chrome.manifest", + "data/base_uri_worker.js", + "data/base_uri_module.mjs", + "data/base_uri_module2.mjs", +] + +["test_ext_redirects_sw_scripts.js"] +# The following firefox-appdir make sure that ExtensionTestUtils.loadExtension +# will be able to successfully start the background page (it does fail without +# it because there wouldn't be a global.tabTracker implementation as we would +# expect in a real Firefox, Fenix or Thunderbird instance). +firefox-appdir = "browser" + +["test_ext_worker_offline_fetch.js"] +firefox-appdir = "browser" + +["test_fileReader.js"] + +["test_import_base_uri.js"] + +["test_remoteworker_launch_new_process.js"] +# The following firefox-appdir make sure that this xpcshell test will run +# with e10s enabled (which is needed to make sure that the test case is +# going to launch the expected new processes) +firefox-appdir = "browser" +# Disable plugin loading to make it rr able to record and replay this test. +prefs = ["plugin.disable=true"] +skip-if = ["socketprocess_networking"] # Bug 1759035 + + +["test_workers.js"] + +["test_workers_clone_error.js"] |