From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../network-observer/test/browser/browser.toml | 33 ++ .../test/browser/browser_networkobserver.js | 72 ++++ .../browser_networkobserver_auth_listener.js | 386 +++++++++++++++++++++ .../browser_networkobserver_invalid_constructor.js | 49 +++ .../browser/browser_networkobserver_override.js | 179 ++++++++++ .../browser_networkobserver_serviceworker.js | 113 ++++++ ...oc_network-observer-missing-service-worker.html | 32 ++ .../test/browser/doc_network-observer.html | 49 +++ .../network-observer/test/browser/gzipped.sjs | 44 +++ .../shared/network-observer/test/browser/head.js | 123 +++++++ .../network-observer/test/browser/override.html | 1 + .../network-observer/test/browser/override.js | 2 + .../network-observer/test/browser/serviceworker.js | 23 ++ .../sjs_network-auth-listener-test-server.sjs | 31 ++ .../browser/sjs_network-observer-test-server.sjs | 196 +++++++++++ 15 files changed, 1333 insertions(+) create mode 100644 devtools/shared/network-observer/test/browser/browser.toml create mode 100644 devtools/shared/network-observer/test/browser/browser_networkobserver.js create mode 100644 devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js create mode 100644 devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js create mode 100644 devtools/shared/network-observer/test/browser/browser_networkobserver_override.js create mode 100644 devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js create mode 100644 devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html create mode 100644 devtools/shared/network-observer/test/browser/doc_network-observer.html create mode 100644 devtools/shared/network-observer/test/browser/gzipped.sjs create mode 100644 devtools/shared/network-observer/test/browser/head.js create mode 100644 devtools/shared/network-observer/test/browser/override.html create mode 100644 devtools/shared/network-observer/test/browser/override.js create mode 100644 devtools/shared/network-observer/test/browser/serviceworker.js create mode 100644 devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs create mode 100644 devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs (limited to 'devtools/shared/network-observer/test/browser') diff --git a/devtools/shared/network-observer/test/browser/browser.toml b/devtools/shared/network-observer/test/browser/browser.toml new file mode 100644 index 0000000000..3b3b44aaae --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser.toml @@ -0,0 +1,33 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "head.js", + "doc_network-observer-missing-service-worker.html", + "doc_network-observer.html", + "gzipped.sjs", + "override.html", + "override.js", + "serviceworker.js", + "sjs_network-auth-listener-test-server.sjs", + "sjs_network-observer-test-server.sjs", +] + +["browser_networkobserver.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] + +["browser_networkobserver_auth_listener.js"] +skip-if = [ + "debug", # Disabled for frequent leaks in Bug 1873571. + "asan", +] + +["browser_networkobserver_invalid_constructor.js"] + +["browser_networkobserver_override.js"] + +["browser_networkobserver_serviceworker.js"] +fail-if = ["true"] # Disabled until Bug 1267119 and Bug 1246289 diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver.js b/devtools/shared/network-observer/test/browser/browser_networkobserver.js new file mode 100644 index 0000000000..8f81ef6f86 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser_networkobserver.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = URL_ROOT + "doc_network-observer.html"; +const REQUEST_URL = + URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=html`; + +// Check that the NetworkObserver can detect basic requests and calls the +// onNetworkEvent callback when expected. +add_task(async function testSingleRequest() { + await addTab(TEST_URL); + + const onNetworkEvents = waitForNetworkEvents(REQUEST_URL, 1); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [REQUEST_URL], _url => { + content.wrappedJSObject.fetch(_url); + }); + + const events = await onNetworkEvents; + is(events.length, 1, "Received the expected number of network events"); +}); + +add_task(async function testMultipleRequests() { + await addTab(TEST_URL); + const EXPECTED_REQUESTS_COUNT = 5; + + const onNetworkEvents = waitForNetworkEvents( + REQUEST_URL, + EXPECTED_REQUESTS_COUNT + ); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [REQUEST_URL, EXPECTED_REQUESTS_COUNT], + (_url, _count) => { + for (let i = 0; i < _count; i++) { + content.wrappedJSObject.fetch(_url); + } + } + ); + + const events = await onNetworkEvents; + is( + events.length, + EXPECTED_REQUESTS_COUNT, + "Received the expected number of network events" + ); +}); + +add_task(async function testOnNetworkEventArguments() { + await addTab(TEST_URL); + + const onNetworkEvent = new Promise(resolve => { + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: () => false, + onNetworkEvent: (...args) => { + resolve(args); + return createNetworkEventOwner(); + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + }); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [REQUEST_URL], _url => { + content.wrappedJSObject.fetch(_url); + }); + + const args = await onNetworkEvent; + is(args.length, 2, "Received two arguments"); + is(typeof args[0], "object", "First argument is an object"); + ok(args[1] instanceof Ci.nsIChannel, "Second argument is a channel"); +}); diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js new file mode 100644 index 0000000000..e3492c10ad --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js @@ -0,0 +1,386 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +const TEST_URL = URL_ROOT + "doc_network-observer.html"; +const AUTH_URL = URL_ROOT + `sjs_network-auth-listener-test-server.sjs`; + +// Correct credentials for sjs_network-auth-listener-test-server.sjs. +const USERNAME = "guest"; +const PASSWORD = "guest"; +const BAD_PASSWORD = "bad"; + +// NetworkEventOwner which will cancel all auth prompt requests. +class AuthCancellingOwner extends NetworkEventOwner { + hasAuthPrompt = false; + + onAuthPrompt(authDetails, authCallbacks) { + this.hasAuthPrompt = true; + authCallbacks.cancelAuthPrompt(); + } +} + +// NetworkEventOwner which will forward all auth prompt requests to the browser. +class AuthForwardingOwner extends NetworkEventOwner { + hasAuthPrompt = false; + + onAuthPrompt(authDetails, authCallbacks) { + this.hasAuthPrompt = true; + authCallbacks.forwardAuthPrompt(); + } +} + +// NetworkEventOwner which will answer provided credentials to auth prompts. +class AuthCredentialsProvidingOwner extends NetworkEventOwner { + hasAuthPrompt = false; + + constructor(channel, username, password) { + super(); + + this.channel = channel; + this.username = username; + this.password = password; + } + + async onAuthPrompt(authDetails, authCallbacks) { + this.hasAuthPrompt = true; + + // Providing credentials immediately can lead to intermittent failures. + // TODO: Investigate and remove. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 100)); + + await authCallbacks.provideAuthCredentials(this.username, this.password); + } + + addResponseContent(content) { + super.addResponseContent(); + this.responseContent = content.text; + } +} + +add_task(async function testAuthRequestWithoutListener() { + cleanupAuthManager(); + const tab = await addTab(TEST_URL); + + const events = []; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL, + onNetworkEvent: event => { + const owner = new AuthForwardingOwner(); + events.push(owner); + return owner; + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + + const onAuthPrompt = waitForAuthPrompt(tab); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => { + content.wrappedJSObject.fetch(_url); + }); + + info("Wait for a network event to be created"); + await BrowserTestUtils.waitForCondition(() => events.length >= 1); + is(events.length, 1, "Received the expected number of network events"); + + info("Wait for the auth prompt to be displayed"); + await onAuthPrompt; + Assert.equal( + getTabAuthPrompts(tab).length, + 1, + "The auth prompt was not blocked by the network observer" + ); + + // The event owner should have been called for ResponseStart and EventTimings + assertEventOwner(events[0], { + hasResponseStart: true, + hasEventTimings: true, + hasServerTimings: true, + }); + + networkObserver.destroy(); + gBrowser.removeTab(tab); +}); + +add_task(async function testAuthRequestWithForwardingListener() { + cleanupAuthManager(); + const tab = await addTab(TEST_URL); + + const events = []; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL, + onNetworkEvent: event => { + info("waitForNetworkEvents received a new event"); + const owner = new AuthForwardingOwner(); + events.push(owner); + return owner; + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + + info("Enable the auth prompt listener for this network observer"); + networkObserver.setAuthPromptListenerEnabled(true); + + const onAuthPrompt = waitForAuthPrompt(tab); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => { + content.wrappedJSObject.fetch(_url); + }); + + info("Wait for a network event to be received"); + await BrowserTestUtils.waitForCondition(() => events.length >= 1); + is(events.length, 1, "Received the expected number of network events"); + + // The auth prompt should still be displayed since the network event owner + // forwards the auth notification immediately. + info("Wait for the auth prompt to be displayed"); + await onAuthPrompt; + Assert.equal( + getTabAuthPrompts(tab).length, + 1, + "The auth prompt was not blocked by the network observer" + ); + + // The event owner should have been called for ResponseStart, EventTimings and + // AuthPrompt + assertEventOwner(events[0], { + hasResponseStart: true, + hasEventTimings: true, + hasAuthPrompt: true, + hasServerTimings: true, + }); + + networkObserver.destroy(); + gBrowser.removeTab(tab); +}); + +add_task(async function testAuthRequestWithCancellingListener() { + cleanupAuthManager(); + const tab = await addTab(TEST_URL); + + const events = []; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL, + onNetworkEvent: event => { + const owner = new AuthCancellingOwner(); + events.push(owner); + return owner; + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + + info("Enable the auth prompt listener for this network observer"); + networkObserver.setAuthPromptListenerEnabled(true); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => { + content.wrappedJSObject.fetch(_url); + }); + + info("Wait for a network event to be received"); + await BrowserTestUtils.waitForCondition(() => events.length >= 1); + is(events.length, 1, "Received the expected number of network events"); + + await BrowserTestUtils.waitForCondition( + () => events[0].hasResponseContent && events[0].hasSecurityInfo + ); + + // The auth prompt should not be displayed since the authentication was + // cancelled. + ok( + !getTabAuthPrompts(tab).length, + "The auth prompt was cancelled by the network event owner" + ); + + assertEventOwner(events[0], { + hasResponseStart: true, + hasResponseContent: true, + hasEventTimings: true, + hasServerTimings: true, + hasAuthPrompt: true, + hasSecurityInfo: true, + }); + + networkObserver.destroy(); + gBrowser.removeTab(tab); +}); + +add_task(async function testAuthRequestWithWrongCredentialsListener() { + cleanupAuthManager(); + const tab = await addTab(TEST_URL); + + const events = []; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL, + onNetworkEvent: (event, channel) => { + const owner = new AuthCredentialsProvidingOwner( + channel, + USERNAME, + BAD_PASSWORD + ); + events.push(owner); + return owner; + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + + info("Enable the auth prompt listener for this network observer"); + networkObserver.setAuthPromptListenerEnabled(true); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => { + content.wrappedJSObject.fetch(_url); + }); + + info("Wait for all network events to be received"); + await BrowserTestUtils.waitForCondition(() => events.length >= 1); + is(events.length, 1, "Received the expected number of network events"); + + // Wait for authPrompt to be handled + await BrowserTestUtils.waitForCondition(() => events[0].hasAuthPrompt); + + // The auth prompt should not be displayed since the authentication was + // fulfilled. + ok( + !getTabAuthPrompts(tab).length, + "The auth prompt was handled by the network event owner" + ); + + assertEventOwner(events[0], { + hasAuthPrompt: true, + hasResponseStart: true, + hasEventTimings: true, + hasServerTimings: true, + }); + + networkObserver.destroy(); + gBrowser.removeTab(tab); +}); + +add_task(async function testAuthRequestWithCredentialsListener() { + cleanupAuthManager(); + const tab = await addTab(TEST_URL); + + const events = []; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== AUTH_URL, + onNetworkEvent: (event, channel) => { + const owner = new AuthCredentialsProvidingOwner( + channel, + USERNAME, + PASSWORD + ); + events.push(owner); + return owner; + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + + info("Enable the auth prompt listener for this network observer"); + networkObserver.setAuthPromptListenerEnabled(true); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [AUTH_URL], _url => { + content.wrappedJSObject.fetch(_url); + }); + + // TODO: At the moment, providing credentials will result in additional + // network events collected by the NetworkObserver, whereas we would expect + // to keep the same event. + // For successful auth prompts, we receive an additional event. + // The last event will contain the responseContent flag. + info("Wait for all network events to be received"); + await BrowserTestUtils.waitForCondition(() => events.length >= 2); + is(events.length, 2, "Received the expected number of network events"); + + // Since the auth prompt was canceled we should also receive the security + // information and the response content. + await BrowserTestUtils.waitForCondition( + () => events[1].hasResponseContent && events[1].hasSecurityInfo + ); + + // The auth prompt should not be displayed since the authentication was + // fulfilled. + ok( + !getTabAuthPrompts(tab).length, + "The auth prompt was handled by the network event owner" + ); + + assertEventOwner(events[1], { + hasResponseStart: true, + hasEventTimings: true, + hasSecurityInfo: true, + hasServerTimings: true, + hasResponseContent: true, + }); + + is(events[1].responseContent, "success", "Auth prompt was successful"); + + networkObserver.destroy(); + gBrowser.removeTab(tab); +}); + +function assertEventOwner(event, expectedFlags) { + is( + event.hasResponseStart, + !!expectedFlags.hasResponseStart, + "network event has the expected ResponseStart flag" + ); + is( + event.hasEventTimings, + !!expectedFlags.hasEventTimings, + "network event has the expected EventTimings flag" + ); + is( + event.hasAuthPrompt, + !!expectedFlags.hasAuthPrompt, + "network event has the expected AuthPrompt flag" + ); + is( + event.hasResponseCache, + !!expectedFlags.hasResponseCache, + "network event has the expected ResponseCache flag" + ); + is( + event.hasResponseContent, + !!expectedFlags.hasResponseContent, + "network event has the expected ResponseContent flag" + ); + is( + event.hasSecurityInfo, + !!expectedFlags.hasSecurityInfo, + "network event has the expected SecurityInfo flag" + ); + is( + event.hasServerTimings, + !!expectedFlags.hasServerTimings, + "network event has the expected ServerTimings flag" + ); +} + +function getTabAuthPrompts(tab) { + const tabDialogBox = gBrowser.getTabDialogBox(tab.linkedBrowser); + return tabDialogBox + .getTabDialogManager() + ._dialogs.filter( + d => d.frameContentWindow?.Dialog.args.promptType == "promptUserAndPass" + ); +} + +function waitForAuthPrompt(tab) { + return PromptTestUtils.waitForPrompt(tab.linkedBrowser, { + modalType: Services.prompt.MODAL_TYPE_TAB, + promptType: "promptUserAndPass", + }); +} + +// Cleanup potentially stored credentials before running any test. +function cleanupAuthManager() { + const authManager = SpecialPowers.Cc[ + "@mozilla.org/network/http-auth-manager;1" + ].getService(SpecialPowers.Ci.nsIHttpAuthManager); + authManager.clearAll(); +} diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js new file mode 100644 index 0000000000..76a93d938a --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the NetworkObserver constructor validates its arguments. +add_task(async function testInvalidConstructorArguments() { + Assert.throws( + () => new NetworkObserver(), + /Expected "ignoreChannelFunction" to be a function, got undefined/, + "NetworkObserver constructor should throw if no argument was provided" + ); + + Assert.throws( + () => new NetworkObserver({}), + /Expected "ignoreChannelFunction" to be a function, got undefined/, + "NetworkObserver constructor should throw if ignoreChannelFunction was not provided" + ); + + const invalidValues = [null, true, false, 12, "str", ["arr"], { obj: "obj" }]; + for (const invalidValue of invalidValues) { + Assert.throws( + () => new NetworkObserver({ ignoreChannelFunction: invalidValue }), + /Expected "ignoreChannelFunction" to be a function, got/, + `NetworkObserver constructor should throw if a(n) ${typeof invalidValue} was provided for ignoreChannelFunction` + ); + } + + const EMPTY_FN = () => {}; + Assert.throws( + () => new NetworkObserver({ ignoreChannelFunction: EMPTY_FN }), + /Expected "onNetworkEvent" to be a function, got undefined/, + "NetworkObserver constructor should throw if onNetworkEvent was not provided" + ); + + // Now we will pass a function for `ignoreChannelFunction`, and will do the + // same tests for onNetworkEvent + for (const invalidValue of invalidValues) { + Assert.throws( + () => + new NetworkObserver({ + ignoreChannelFunction: EMPTY_FN, + onNetworkEvent: invalidValue, + }), + /Expected "onNetworkEvent" to be a function, got/, + `NetworkObserver constructor should throw if a(n) ${typeof invalidValue} was provided for onNetworkEvent` + ); + } +}); diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js new file mode 100644 index 0000000000..3b00c4b2e9 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_override.js @@ -0,0 +1,179 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = URL_ROOT + "doc_network-observer.html"; +const REQUEST_URL = + URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=html`; +const GZIPPED_REQUEST_URL = URL_ROOT + `gzipped.sjs`; +const OVERRIDE_FILENAME = "override.js"; +const OVERRIDE_HTML_FILENAME = "override.html"; + +add_task(async function testLocalOverride() { + await addTab(TEST_URL); + + let eventsCount = 0; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => channel.URI.spec !== REQUEST_URL, + onNetworkEvent: event => { + info("received a network event"); + eventsCount++; + return createNetworkEventOwner(event); + }, + }); + + const overrideFile = getChromeDir(getResolvedURI(gTestPath)); + overrideFile.append(OVERRIDE_FILENAME); + info(" override " + REQUEST_URL + " to " + overrideFile.path + "\n"); + networkObserver.override(REQUEST_URL, overrideFile.path); + + info("Assert that request and cached request are overriden"); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [REQUEST_URL], + async _url => { + const request = await content.wrappedJSObject.fetch(_url); + const requestcontent = await request.text(); + is( + requestcontent, + `"use strict";\ndocument.title = "evaluated";\n`, + "the request content has been overriden" + ); + const secondRequest = await content.wrappedJSObject.fetch(_url); + const secondRequestcontent = await secondRequest.text(); + is( + secondRequestcontent, + `"use strict";\ndocument.title = "evaluated";\n`, + "the cached request content has been overriden" + ); + } + ); + + info("Assert that JS scripts can be overriden"); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [REQUEST_URL], + async _url => { + const script = await content.document.createElement("script"); + const onLoad = new Promise(resolve => + script.addEventListener("load", resolve, { once: true }) + ); + script.src = _url; + content.document.body.appendChild(script); + await onLoad; + is( + content.document.title, + "evaluated", + "The + + diff --git a/devtools/shared/network-observer/test/browser/doc_network-observer.html b/devtools/shared/network-observer/test/browser/doc_network-observer.html new file mode 100644 index 0000000000..2ca400e0ae --- /dev/null +++ b/devtools/shared/network-observer/test/browser/doc_network-observer.html @@ -0,0 +1,49 @@ + + + + + + + + + + Network Observer test page + + +

Network Observer test page

+ + + diff --git a/devtools/shared/network-observer/test/browser/gzipped.sjs b/devtools/shared/network-observer/test/browser/gzipped.sjs new file mode 100644 index 0000000000..09d0b249b1 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/gzipped.sjs @@ -0,0 +1,44 @@ +"use strict"; + +function gzipCompressString(string, obs) { + const scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( + Ci.nsIStreamLoader + ); + listener.init(obs); + const converter = scs.asyncConvertData( + "uncompressed", + "gzip", + listener, + null + ); + const stringStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + stringStream.data = string; + converter.onStartRequest(null, null); + converter.onDataAvailable(null, stringStream, 0, string.length); + converter.onStopRequest(null, null, null); +} + +const ORIGINAL_JS_CONTENT = `console.log("original javascript content");`; + +function handleRequest(request, response) { + response.processAsync(); + + // Generate data + response.setHeader("Content-Type", "application/javascript", false); + response.setHeader("Content-Encoding", "gzip", false); + + const observer = { + onStreamComplete(loader, context, status, length, result) { + const buffer = String.fromCharCode.apply(this, result); + response.setHeader("Content-Length", "" + buffer.length, false); + response.write(buffer); + response.finish(); + }, + }; + gzipCompressString(ORIGINAL_JS_CONTENT, observer); +} diff --git a/devtools/shared/network-observer/test/browser/head.js b/devtools/shared/network-observer/test/browser/head.js new file mode 100644 index 0000000000..deb7becff6 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/head.js @@ -0,0 +1,123 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + NetworkObserver: + "resource://devtools/shared/network-observer/NetworkObserver.sys.mjs", +}); + +const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/")); +const CHROME_URL_ROOT = TEST_DIR + "/"; +const URL_ROOT = CHROME_URL_ROOT.replace( + "chrome://mochitests/content/", + "https://example.com/" +); + +/** + * Load the provided url in an existing browser. + * Returns a promise which will resolve when the page is loaded. + * + * @param {Browser} browser + * The browser element where the URL should be loaded. + * @param {String} url + * The URL to load in the new tab + */ +async function loadURL(browser, url) { + const loaded = BrowserTestUtils.browserLoaded(browser); + BrowserTestUtils.startLoadingURIString(browser, url); + return loaded; +} + +/** + * Create a new foreground tab loading the provided url. + * Returns a promise which will resolve when the page is loaded. + * + * @param {String} url + * The URL to load in the new tab + */ +async function addTab(url) { + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + registerCleanupFunction(() => { + gBrowser.removeTab(tab); + }); + return tab; +} + +/** + * Base network event owner class implementing all mandatory callbacks and + * keeping track of which callbacks have been called. + */ +class NetworkEventOwner { + hasEventTimings = false; + hasResponseCache = false; + hasResponseContent = false; + hasResponseStart = false; + hasSecurityInfo = false; + hasServerTimings = false; + + addEventTimings() { + this.hasEventTimings = true; + } + addResponseCache() { + this.hasResponseCache = true; + } + addResponseContent() { + this.hasResponseContent = true; + } + addResponseStart() { + this.hasResponseStart = true; + } + addSecurityInfo() { + this.hasSecurityInfo = true; + } + addServerTimings() { + this.hasServerTimings = true; + } + addServiceWorkerTimings() { + this.hasServiceWorkerTimings = true; + } +} + +/** + * Create a simple network event owner, with mock implementations of all + * the expected APIs for a NetworkEventOwner. + */ +function createNetworkEventOwner(event) { + return new NetworkEventOwner(); +} + +/** + * Wait for network events matching the provided URL, until the count reaches + * the provided expected count. + * + * @param {string|null} expectedUrl + * The URL which should be monitored by the NetworkObserver.If set to null watch for + * all requests + * @param {number} expectedRequestsCount + * How many different events (requests) are expected. + * @returns {Promise} + * A promise which will resolve with an array of network event owners, when + * the expected event count is reached. + */ +async function waitForNetworkEvents(expectedUrl = null, expectedRequestsCount) { + const events = []; + const networkObserver = new NetworkObserver({ + ignoreChannelFunction: channel => + expectedUrl ? channel.URI.spec !== expectedUrl : false, + onNetworkEvent: () => { + info("waitForNetworkEvents received a new event"); + const owner = createNetworkEventOwner(); + events.push(owner); + return owner; + }, + }); + registerCleanupFunction(() => networkObserver.destroy()); + + info("Wait until the events count reaches " + expectedRequestsCount); + await BrowserTestUtils.waitForCondition( + () => events.length >= expectedRequestsCount + ); + return events; +} diff --git a/devtools/shared/network-observer/test/browser/override.html b/devtools/shared/network-observer/test/browser/override.html new file mode 100644 index 0000000000..0e3878e313 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/override.html @@ -0,0 +1 @@ +Overriden! diff --git a/devtools/shared/network-observer/test/browser/override.js b/devtools/shared/network-observer/test/browser/override.js new file mode 100644 index 0000000000..7b000fcd0f --- /dev/null +++ b/devtools/shared/network-observer/test/browser/override.js @@ -0,0 +1,2 @@ +"use strict"; +document.title = "evaluated"; diff --git a/devtools/shared/network-observer/test/browser/serviceworker.js b/devtools/shared/network-observer/test/browser/serviceworker.js new file mode 100644 index 0000000000..3389581fb0 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/serviceworker.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +self.addEventListener("activate", async event => { + ( + await fetch( + "https://example.com/browser/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs?sts=200&fmt=json" + ) + ) + .json() + .then(() => console.log("json downloaded")); + // start controlling the already loaded page + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("fetch", event => { + const response = new Response("Service worker response", { + statusText: "OK", + }); + event.respondWith(response); +}); diff --git a/devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs b/devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs new file mode 100644 index 0000000000..028a26ebfe --- /dev/null +++ b/devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function handleRequest(request, response) { + let body; + + // Expect guest/guest as correct credentials, but `btoa` is unavailable in sjs + // "Z3Vlc3Q6Z3Vlc3Q=" == btoa("guest:guest") + const expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + // correct login credentials provided + if ( + request.hasHeader("Authorization") && + request.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(request.httpVersion, 200, "OK, authorized"); + response.setHeader("Content-Type", "text", false); + + body = "success"; + } else { + // incorrect credentials + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + response.setHeader("Content-Type", "text", false); + + body = "failed"; + } + response.bodyOutputStream.write(body, body.length); +} diff --git a/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs b/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs new file mode 100644 index 0000000000..b0947cadd1 --- /dev/null +++ b/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs @@ -0,0 +1,196 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Simple server which can handle several response types and states. +// Trimmed down from devtools/client/netmonitor/test/sjs_content-type-test-server.sjs +// Additional features can be ported if needed. +function handleRequest(request, response) { + response.processAsync(); + + const params = request.queryString.split("&"); + const format = (params.filter(s => s.includes("fmt="))[0] || "").split( + "=" + )[1]; + const status = + (params.filter(s => s.includes("sts="))[0] || "").split("=")[1] || 200; + + const cacheExpire = 60; // seconds + + function setCacheHeaders() { + if (status != 304) { + response.setHeader( + "Cache-Control", + "no-cache, no-store, must-revalidate" + ); + response.setHeader("Pragma", "no-cache"); + response.setHeader("Expires", "0"); + return; + } + + response.setHeader("Expires", Date(Date.now() + cacheExpire * 1000), false); + } + + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + timer.initWithCallback( + // eslint-disable-next-line complexity + () => { + // to avoid garbage collection + timer = null; + switch (format) { + case "txt": { + response.setStatusLine(request.httpVersion, status, "DA DA DA"); + response.setHeader("Content-Type", "text/plain", false); + setCacheHeaders(); + + function convertToUtf8(str) { + return String.fromCharCode(...new TextEncoder().encode(str)); + } + + // This script must be evaluated as UTF-8 for this to write out the + // bytes of the string in UTF-8. If it's evaluated as Latin-1, the + // written bytes will be the result of UTF-8-encoding this string + // *twice*. + const data = "Братан, ты вообще качаешься?"; + const stringOfUtf8Bytes = convertToUtf8(data); + response.write(stringOfUtf8Bytes); + + response.finish(); + break; + } + case "xml": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/xml; charset=utf-8", false); + setCacheHeaders(); + response.write(""); + response.finish(); + break; + } + case "html": { + const content = ( + params.filter(s => s.includes("res="))[0] || "" + ).split("=")[1]; + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setCacheHeaders(); + response.write(content || "

Hello HTML!

"); + response.finish(); + break; + } + case "xhtml": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/xhtml+xml; charset=utf-8", + false + ); + setCacheHeaders(); + response.write("

Hello XHTML!

"); + response.finish(); + break; + } + case "html-long": { + const str = new Array(102400 /* 100 KB in bytes */).join("."); + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setCacheHeaders(); + response.write("

" + str + "

"); + response.finish(); + break; + } + case "css": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "text/css; charset=utf-8", false); + setCacheHeaders(); + response.write("body:pre { content: 'Hello CSS!' }"); + response.finish(); + break; + } + case "js": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/javascript; charset=utf-8", + false + ); + setCacheHeaders(); + response.write("function() { return 'Hello JS!'; }"); + response.finish(); + break; + } + case "json": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader( + "Content-Type", + "application/json; charset=utf-8", + false + ); + setCacheHeaders(); + response.write('{ "greeting": "Hello JSON!" }'); + response.finish(); + break; + } + + case "font": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "font/woff", false); + setCacheHeaders(); + response.finish(); + break; + } + case "image": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "image/png", false); + setCacheHeaders(); + response.finish(); + break; + } + case "application-ogg": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "application/ogg", false); + setCacheHeaders(); + response.finish(); + break; + } + case "audio": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "audio/ogg", false); + setCacheHeaders(); + response.finish(); + break; + } + case "video": { + response.setStatusLine(request.httpVersion, status, "OK"); + response.setHeader("Content-Type", "video/webm", false); + setCacheHeaders(); + response.finish(); + break; + } + case "ws": { + response.setStatusLine( + request.httpVersion, + 101, + "Switching Protocols" + ); + response.setHeader("Connection", "upgrade", false); + response.setHeader("Upgrade", "websocket", false); + setCacheHeaders(); + response.finish(); + break; + } + default: { + response.setStatusLine(request.httpVersion, 404, "Not Found"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + setCacheHeaders(); + response.write("Not Found"); + response.finish(); + break; + } + } + }, + 10, + Ci.nsITimer.TYPE_ONE_SHOT + ); // Make sure this request takes a few ms. +} -- cgit v1.2.3