summaryrefslogtreecommitdiffstats
path: root/devtools/shared/network-observer/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/network-observer/test')
-rw-r--r--devtools/shared/network-observer/test/browser/browser.toml33
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver.js72
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver_auth_listener.js386
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver_invalid_constructor.js49
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver_override.js179
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js113
-rw-r--r--devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html32
-rw-r--r--devtools/shared/network-observer/test/browser/doc_network-observer.html49
-rw-r--r--devtools/shared/network-observer/test/browser/gzipped.sjs44
-rw-r--r--devtools/shared/network-observer/test/browser/head.js123
-rw-r--r--devtools/shared/network-observer/test/browser/override.html1
-rw-r--r--devtools/shared/network-observer/test/browser/override.js2
-rw-r--r--devtools/shared/network-observer/test/browser/serviceworker.js23
-rw-r--r--devtools/shared/network-observer/test/browser/sjs_network-auth-listener-test-server.sjs31
-rw-r--r--devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs196
-rw-r--r--devtools/shared/network-observer/test/xpcshell/head.js9
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_network_helper.js90
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-certificate.js84
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-parser.js54
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-protocol-version.js48
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-state.js122
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-static-hpkp.js41
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_security-info-weakness-reasons.js39
-rw-r--r--devtools/shared/network-observer/test/xpcshell/test_throttle.js162
-rw-r--r--devtools/shared/network-observer/test/xpcshell/xpcshell.toml22
25 files changed, 2004 insertions, 0 deletions
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 <script> tag content has been overriden and correctly evaluated"
+ );
+ }
+ );
+
+ await BrowserTestUtils.waitForCondition(() => eventsCount >= 1);
+
+ networkObserver.destroy();
+});
+
+add_task(async function testHtmlFileOverride() {
+ let eventsCount = 0;
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== TEST_URL,
+ onNetworkEvent: event => {
+ info("received a network event");
+ eventsCount++;
+ return createNetworkEventOwner(event);
+ },
+ });
+
+ const overrideFile = getChromeDir(getResolvedURI(gTestPath));
+ overrideFile.append(OVERRIDE_HTML_FILENAME);
+ info(" override " + TEST_URL + " to " + overrideFile.path + "\n");
+ networkObserver.override(TEST_URL, overrideFile.path);
+
+ await addTab(TEST_URL);
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [TEST_URL],
+ async pageUrl => {
+ is(
+ content.document.documentElement.outerHTML,
+ "<html><head></head><body>Overriden!\n</body></html>",
+ "The content of the HTML has been overriden"
+ );
+ // For now, all overriden request have their location changed to an internal data: URI
+ // Bug xxx aims at keeping the original URI.
+ todo_is(
+ content.location.href,
+ pageUrl,
+ "The location of the page is still the original one"
+ );
+ }
+ );
+ await BrowserTestUtils.waitForCondition(() => eventsCount >= 1);
+ networkObserver.destroy();
+});
+
+// Exact same test, but with a gzipped request, which requires very special treatment
+add_task(async function testLocalOverrideGzipped() {
+ await addTab(TEST_URL);
+
+ let eventsCount = 0;
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== GZIPPED_REQUEST_URL,
+ onNetworkEvent: event => {
+ info("received a network event");
+ eventsCount++;
+ return createNetworkEventOwner(event);
+ },
+ });
+
+ const overrideFile = getChromeDir(getResolvedURI(gTestPath));
+ overrideFile.append(OVERRIDE_FILENAME);
+ info(" override " + GZIPPED_REQUEST_URL + " to " + overrideFile.path + "\n");
+ networkObserver.override(GZIPPED_REQUEST_URL, overrideFile.path);
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [GZIPPED_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"
+ );
+ }
+ );
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [GZIPPED_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 <script> tag content has been overriden and correctly evaluated"
+ );
+ }
+ );
+
+ await BrowserTestUtils.waitForCondition(() => eventsCount >= 1);
+
+ networkObserver.destroy();
+});
diff --git a/devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js b/devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js
new file mode 100644
index 0000000000..1680dc6005
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/browser_networkobserver_serviceworker.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that all the expected service worker requests are received
+// by the network observer.
+add_task(async function testServiceWorkerSuccessRequests() {
+ await addTab(URL_ROOT + "doc_network-observer.html");
+
+ const REQUEST_URL =
+ URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=`;
+
+ const EXPECTED_REQUESTS = [
+ // The main service worker script request
+ `https://example.com/browser/devtools/shared/network-observer/test/browser/serviceworker.js`,
+ // The requests intercepted by the service worker
+ REQUEST_URL + "js",
+ REQUEST_URL + "css",
+ // The request initiated by the service worker
+ REQUEST_URL + "json",
+ ];
+
+ const onNetworkEvents = waitForNetworkEvents(null, 4);
+
+ info("Register the service worker and send requests...");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [REQUEST_URL],
+ async url => {
+ await content.wrappedJSObject.registerServiceWorker();
+ content.wrappedJSObject.fetch(url + "js");
+ content.wrappedJSObject.fetch(url + "css");
+ }
+ );
+ const events = await onNetworkEvents;
+
+ is(events.length, 4, "Received the expected number of network events");
+ for (const { options, channel } of events) {
+ info(`Assert the info for the request from ${channel.URI.spec}`);
+ ok(
+ EXPECTED_REQUESTS.includes(channel.URI.spec),
+ `The request for ${channel.URI.spec} is an expected service worker request`
+ );
+ Assert.notStrictEqual(
+ channel.loadInfo.browsingContextID,
+ 0,
+ `The request for ${channel.URI.spec} has a Browsing Context ID of ${channel.loadInfo.browsingContextID}`
+ );
+ // The main service worker script request is not from the service worker
+ if (channel.URI.spec.includes("serviceworker.js")) {
+ ok(
+ !options.fromServiceWorker,
+ `The request for ${channel.URI.spec} is not from the service worker\n`
+ );
+ } else {
+ ok(
+ options.fromServiceWorker,
+ `The request for ${channel.URI.spec} is from the service worker\n`
+ );
+ }
+ }
+
+ info("Unregistering the service worker...");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ await content.wrappedJSObject.unregisterServiceWorker();
+ });
+});
+
+// Tests that the expected failed service worker request is received by the network observer.
+add_task(async function testServiceWorkerFailedRequests() {
+ await addTab(URL_ROOT + "doc_network-observer-missing-service-worker.html");
+
+ const REQUEST_URL =
+ URL_ROOT + `sjs_network-observer-test-server.sjs?sts=200&fmt=js`;
+
+ const EXPECTED_REQUESTS = [
+ // The main service worker script request which should be missing
+ "https://example.com/browser/devtools/shared/network-observer/test/browser/serviceworker-missing.js",
+ // A notrmal request
+ "https://example.com/browser/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs?sts=200&fmt=js",
+ ];
+
+ const onNetworkEvents = waitForNetworkEvents(null, 2);
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [REQUEST_URL],
+ async url => {
+ await content.wrappedJSObject.registerServiceWorker();
+ content.wrappedJSObject.fetch(url);
+ }
+ );
+
+ const events = await onNetworkEvents;
+ is(events.length, 2, "Received the expected number of network events");
+
+ for (const { options, channel } of events) {
+ info(`Assert the info for the request from ${channel.URI.spec}`);
+ ok(
+ EXPECTED_REQUESTS.includes(channel.URI.spec),
+ `The request for ${channel.URI.spec} is an expected request`
+ );
+ Assert.notStrictEqual(
+ channel.loadInfo.browsingContextID,
+ 0,
+ `The request for ${channel.URI.spec} has a Browsing Context ID of ${channel.loadInfo.browsingContextID}`
+ );
+ ok(
+ !options.fromServiceWorker,
+ `The request for ${channel.URI.spec} is not from the service worker\n`
+ );
+ }
+});
diff --git a/devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html b/devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html
new file mode 100644
index 0000000000..396e51677c
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/doc_network-observer-missing-service-worker.html
@@ -0,0 +1,32 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Observer missing service worker test page</title>
+ </head>
+
+ <body>
+ <p>Network Observer test page</p>
+ <script type="text/javascript">
+ /* exported registerServiceWorker */
+ "use strict";
+
+ function registerServiceWorker() {
+ const sw = navigator.serviceWorker;
+ // NOTE: This service worker file does not exist which enables testing
+ // that a 404 requests is received.
+ return sw.register("serviceworker-missing.js")
+ .then(registration => {
+ throw new Error("The Service Worker file should not exist");
+ }).catch(err => {
+ console.log("Registration failed as expected");
+ });
+ }
+ </script>
+ </body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+ <meta http-equiv="Pragma" content="no-cache" />
+ <meta http-equiv="Expires" content="0" />
+ <title>Network Observer test page</title>
+ </head>
+ <body>
+ <p>Network Observer test page</p>
+ <script type="text/javascript">
+ /* exported registerServiceWorker, unregisterServiceWorker */
+ "use strict";
+
+ let swRegistration;
+
+ function registerServiceWorker() {
+ const sw = navigator.serviceWorker;
+ return sw.register("serviceworker.js")
+ .then(registration => {
+ swRegistration = registration;
+ console.log("Registered, scope is:", registration.scope);
+ return sw.ready;
+ }).then(() => {
+ // wait until the page is controlled
+ return new Promise(resolve => {
+ if (sw.controller) {
+ resolve();
+ } else {
+ sw.addEventListener("controllerchange", function () {
+ resolve();
+ }, { once: true });
+ }
+ });
+ }).catch(err => {
+ console.error("Registration failed");
+ });
+ }
+
+ function unregisterServiceWorker() {
+ return swRegistration.unregister();
+ }
+ </script>
+ </body>
+</html>
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 @@
+<html><head></head><body>Overriden!</body></html>
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("<label value='greeting'>Hello XML!</label>");
+ 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 || "<p>Hello HTML!</p>");
+ 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("<p>Hello XHTML!</p>");
+ 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("<p>" + str + "</p>");
+ 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("<blink>Not Found</blink>");
+ response.finish();
+ break;
+ }
+ }
+ },
+ 10,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ ); // Make sure this request takes a few ms.
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/head.js b/devtools/shared/network-observer/test/xpcshell/head.js
new file mode 100644
index 0000000000..93b66e4632
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/head.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ NetworkHelper:
+ "resource://devtools/shared/network-observer/NetworkHelper.sys.mjs",
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/test_network_helper.js b/devtools/shared/network-observer/test/xpcshell/test_network_helper.js
new file mode 100644
index 0000000000..ff514ab98e
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_network_helper.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function run_test() {
+ test_isTextMimeType();
+ test_parseCookieHeader();
+}
+
+function test_isTextMimeType() {
+ Assert.equal(NetworkHelper.isTextMimeType("text/plain"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("application/javascript"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("application/json"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("text/css"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("text/html"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("image/svg+xml"), true);
+ Assert.equal(NetworkHelper.isTextMimeType("application/xml"), true);
+
+ // Test custom JSON subtype
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0+json"),
+ true
+ );
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0-json"),
+ true
+ );
+ // Test custom XML subtype
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0+xml"),
+ true
+ );
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0-xml"),
+ false
+ );
+ // Test case-insensitive
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.BIG-CORP+json"),
+ true
+ );
+ // Test non-text type
+ Assert.equal(NetworkHelper.isTextMimeType("image/png"), false);
+ // Test invalid types
+ Assert.equal(NetworkHelper.isTextMimeType("application/foo-+json"), false);
+ Assert.equal(NetworkHelper.isTextMimeType("application/-foo+json"), false);
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/foo--bar+json"),
+ false
+ );
+
+ // Test we do not cause internal errors with unoptimized regex. Bug 961097
+ Assert.equal(
+ NetworkHelper.isTextMimeType("application/vnd.google.safebrowsing-chunk"),
+ false
+ );
+}
+
+function test_parseCookieHeader() {
+ let result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=Strict"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Strict" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=strict"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Strict" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=STRICT"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Strict" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=None"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "None" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=NONE"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "None" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=lax"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Lax" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders(["Test=1; SameSite=Lax"]);
+ Assert.deepEqual(result, [{ name: "Test", value: "1", samesite: "Lax" }]);
+
+ result = NetworkHelper.parseSetCookieHeaders([
+ "Test=1; SameSite=Lax",
+ "Foo=2; SameSite=None",
+ ]);
+ Assert.deepEqual(result, [
+ { name: "Test", value: "1", samesite: "Lax" },
+ { name: "Foo", value: "2", samesite: "None" },
+ ]);
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-certificate.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-certificate.js
new file mode 100644
index 0000000000..00f482b8ca
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-certificate.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that NetworkHelper.parseCertificateInfo parses certificate information
+// correctly.
+
+const DUMMY_CERT = {
+ getBase64DERString() {
+ // This is the base64-encoded contents of the "DigiCert ECC Secure Server CA"
+ // intermediate certificate as issued by "DigiCert Global Root CA". It was
+ // chosen as a test certificate because it has an issuer common name,
+ // organization, and organizational unit that are somewhat distinct from
+ // its subject common name and organization name.
+ return "MIIDrDCCApSgAwIBAgIQCssoukZe5TkIdnRw883GEjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaMEwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJjAkBgNVBAMTHURpZ2lDZXJ0IEVDQyBTZWN1cmUgU2VydmVyIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ghC6nfYJN6gLGSkE85AnCNyqQIKDjc/ITa4jVMU9tWRlUvzlgKNcR7E2Munn17voOZ/WpIRllNv68DLP679Wz9HJOeaBy6Wvqgvu1cYr3GkvXg6HuhbPGtkESvMNCuMo4IBITCCAR0wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAENjA0MDIGBFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAdBgNVHQ4EFgQUo53mH/naOU/AbuiRy5Wl2jHiCp8wHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEMBQADggEBAMeKoENL7HTJxavVHzA1Nm6YVntIrAVjrnuaVyRXzG/63qttnMe2uuzO58pzZNvfBDcKAEmzP58mrZGMIOgfiA4q+2Y3yDDo0sIkp0VILeoBUEoxlBPfjV/aKrtJPGHzecicZpIalir0ezZYoyxBEHQa0+1IttK7igZFcTMQMHp6mCHdJLnsnLWSB62DxsRq+HfmNb4TDydkskO/g+l3VtsIh5RHFPVfKK+jaEyDj2D3loB5hWp2Jp2VDCADjT7ueihlZGak2YPqmXTNbk19HOuNssWvFhtOyPNV6og4ETQdEa8/B6hPatJ0ES8q/HO3X8IVQwVs1n3aAr0im0/T+Xc=";
+ },
+};
+
+add_task(async function run_test() {
+ info("Testing NetworkHelper.parseCertificateInfo.");
+
+ const result = await NetworkHelper.parseCertificateInfo(
+ DUMMY_CERT,
+ new Map()
+ );
+
+ // Subject
+ equal(
+ result.subject.commonName,
+ "DigiCert ECC Secure Server CA",
+ "Common name is correct."
+ );
+ equal(
+ result.subject.organization,
+ "DigiCert Inc",
+ "Organization is correct."
+ );
+ equal(
+ result.subject.organizationUnit,
+ undefined,
+ "Organizational unit is correct."
+ );
+
+ // Issuer
+ equal(
+ result.issuer.commonName,
+ "DigiCert Global Root CA",
+ "Common name of the issuer is correct."
+ );
+ equal(
+ result.issuer.organization,
+ "DigiCert Inc",
+ "Organization of the issuer is correct."
+ );
+ equal(
+ result.issuer.organizationUnit,
+ "www.digicert.com",
+ "Organizational unit of the issuer is correct."
+ );
+
+ // Validity
+ equal(
+ result.validity.start,
+ "Fri, 08 Mar 2013 12:00:00 GMT",
+ "Start of the validity period is correct."
+ );
+ equal(
+ result.validity.end,
+ "Wed, 08 Mar 2023 12:00:00 GMT",
+ "End of the validity period is correct."
+ );
+
+ // Fingerprints
+ equal(
+ result.fingerprint.sha1,
+ "56:EE:7C:27:06:83:16:2D:83:BA:EA:CC:79:0E:22:47:1A:DA:AB:E8",
+ "Certificate SHA1 fingerprint is correct."
+ );
+ equal(
+ result.fingerprint.sha256,
+ "45:84:46:BA:75:D9:32:E9:14:F2:3C:2B:57:B7:D1:92:ED:DB:C2:18:1D:95:8E:11:81:AD:52:51:74:7A:1E:E8",
+ "Certificate SHA256 fingerprint is correct."
+ );
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-parser.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-parser.js
new file mode 100644
index 0000000000..9515851a8b
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-parser.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that NetworkHelper.parseSecurityInfo returns correctly formatted object.
+
+const wpl = Ci.nsIWebProgressListener;
+const MockCertificate = {
+ getBase64DERString() {
+ // This is the same test certificate as in
+ // test_security-info-certificate.js for consistency.
+ return "MIIDrDCCApSgAwIBAgIQCssoukZe5TkIdnRw883GEjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaMEwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJjAkBgNVBAMTHURpZ2lDZXJ0IEVDQyBTZWN1cmUgU2VydmVyIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ghC6nfYJN6gLGSkE85AnCNyqQIKDjc/ITa4jVMU9tWRlUvzlgKNcR7E2Munn17voOZ/WpIRllNv68DLP679Wz9HJOeaBy6Wvqgvu1cYr3GkvXg6HuhbPGtkESvMNCuMo4IBITCCAR0wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAENjA0MDIGBFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAdBgNVHQ4EFgQUo53mH/naOU/AbuiRy5Wl2jHiCp8wHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEMBQADggEBAMeKoENL7HTJxavVHzA1Nm6YVntIrAVjrnuaVyRXzG/63qttnMe2uuzO58pzZNvfBDcKAEmzP58mrZGMIOgfiA4q+2Y3yDDo0sIkp0VILeoBUEoxlBPfjV/aKrtJPGHzecicZpIalir0ezZYoyxBEHQa0+1IttK7igZFcTMQMHp6mCHdJLnsnLWSB62DxsRq+HfmNb4TDydkskO/g+l3VtsIh5RHFPVfKK+jaEyDj2D3loB5hWp2Jp2VDCADjT7ueihlZGak2YPqmXTNbk19HOuNssWvFhtOyPNV6og4ETQdEa8/B6hPatJ0ES8q/HO3X8IVQwVs1n3aAr0im0/T+Xc=";
+ },
+};
+
+// This *cannot* be used as an nsITransportSecurityInfo (since that interface is
+// builtinclass) but the methods being tested aren't defined by XPCOM and aren't
+// calling QueryInterface, so this usage is fine.
+const MockSecurityInfo = {
+ securityState: wpl.STATE_IS_SECURE,
+ errorCode: 0,
+ cipherName: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+ // TLS_VERSION_1_2
+ protocolVersion: 3,
+ serverCert: MockCertificate,
+};
+
+add_task(async function run_test() {
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+
+ equal(result.state, "secure", "State is correct.");
+
+ equal(
+ result.cipherSuite,
+ MockSecurityInfo.cipherName,
+ "Cipher suite is correct."
+ );
+
+ equal(result.protocolVersion, "TLSv1.2", "Protocol version is correct.");
+
+ deepEqual(
+ result.cert,
+ await NetworkHelper.parseCertificateInfo(MockCertificate, new Map()),
+ "Certificate information is correct."
+ );
+
+ equal(result.hpkp, false, "HPKP is false when URI is not available.");
+ equal(result.hsts, false, "HSTS is false when URI is not available.");
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-protocol-version.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-protocol-version.js
new file mode 100644
index 0000000000..a81f7ce73c
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-protocol-version.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that NetworkHelper.formatSecurityProtocol returns correct
+// protocol version strings.
+
+const TEST_CASES = [
+ {
+ description: "TLS_VERSION_1",
+ input: 1,
+ expected: "TLSv1",
+ },
+ {
+ description: "TLS_VERSION_1.1",
+ input: 2,
+ expected: "TLSv1.1",
+ },
+ {
+ description: "TLS_VERSION_1.2",
+ input: 3,
+ expected: "TLSv1.2",
+ },
+ {
+ description: "TLS_VERSION_1.3",
+ input: 4,
+ expected: "TLSv1.3",
+ },
+ {
+ description: "invalid version",
+ input: -1,
+ expected: "Unknown",
+ },
+];
+
+function run_test() {
+ info("Testing NetworkHelper.formatSecurityProtocol.");
+
+ for (const { description, input, expected } of TEST_CASES) {
+ info("Testing " + description);
+
+ equal(
+ NetworkHelper.formatSecurityProtocol(input),
+ expected,
+ "Got the expected protocol string."
+ );
+ }
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-state.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-state.js
new file mode 100644
index 0000000000..be622b2019
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-state.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that security info parser gives correct general security state for
+// different cases.
+
+const wpl = Ci.nsIWebProgressListener;
+
+// This *cannot* be used as an nsITransportSecurityInfo (since that interface is
+// builtinclass) but the methods being tested aren't defined by XPCOM and aren't
+// calling QueryInterface, so this usage is fine.
+const MockSecurityInfo = {
+ securityState: wpl.STATE_IS_BROKEN,
+ errorCode: 0,
+ // nsISSLStatus.TLS_VERSION_1_2
+ protocolVersion: 3,
+ cipherName: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+};
+
+add_task(async function run_test() {
+ await test_nullSecurityInfo();
+ await test_insecureSecurityInfoWithNSSError();
+ await test_insecureSecurityInfoWithoutNSSError();
+ await test_brokenSecurityInfo();
+ await test_secureSecurityInfo();
+});
+
+/**
+ * Test that undefined security information is returns "insecure".
+ */
+async function test_nullSecurityInfo() {
+ const result = await NetworkHelper.parseSecurityInfo(null, {}, {}, new Map());
+ equal(
+ result.state,
+ "insecure",
+ "state == 'insecure' when securityInfo was undefined"
+ );
+}
+
+/**
+ * Test that STATE_IS_INSECURE with NSSError returns "broken"
+ */
+async function test_insecureSecurityInfoWithNSSError() {
+ MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
+
+ // Taken from security/manager/ssl/tests/unit/head_psm.js.
+ MockSecurityInfo.errorCode = -8180;
+
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+ equal(
+ result.state,
+ "broken",
+ "state == 'broken' if securityState contains STATE_IS_INSECURE flag AND " +
+ "errorCode is NSS error."
+ );
+
+ MockSecurityInfo.errorCode = 0;
+}
+
+/**
+ * Test that STATE_IS_INSECURE without NSSError returns "insecure"
+ */
+async function test_insecureSecurityInfoWithoutNSSError() {
+ MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
+
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+ equal(
+ result.state,
+ "insecure",
+ "state == 'insecure' if securityState contains STATE_IS_INSECURE flag BUT " +
+ "errorCode is not NSS error."
+ );
+}
+
+/**
+ * Test that STATE_IS_SECURE returns "secure"
+ */
+async function test_secureSecurityInfo() {
+ MockSecurityInfo.securityState = wpl.STATE_IS_SECURE;
+
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+ equal(
+ result.state,
+ "secure",
+ "state == 'secure' if securityState contains STATE_IS_SECURE flag"
+ );
+}
+
+/**
+ * Test that STATE_IS_BROKEN returns "weak"
+ */
+async function test_brokenSecurityInfo() {
+ MockSecurityInfo.securityState = wpl.STATE_IS_BROKEN;
+
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ {},
+ new Map()
+ );
+ equal(
+ result.state,
+ "weak",
+ "state == 'weak' if securityState contains STATE_IS_BROKEN flag"
+ );
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-static-hpkp.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-static-hpkp.js
new file mode 100644
index 0000000000..f1aa883b93
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-static-hpkp.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that NetworkHelper.parseSecurityInfo correctly detects static hpkp pins
+
+const wpl = Ci.nsIWebProgressListener;
+
+// This *cannot* be used as an nsITransportSecurityInfo (since that interface is
+// builtinclass) but the methods being tested aren't defined by XPCOM and aren't
+// calling QueryInterface, so this usage is fine.
+const MockSecurityInfo = {
+ securityState: wpl.STATE_IS_SECURE,
+ errorCode: 0,
+ cipherName: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+ // TLS_VERSION_1_2
+ protocolVersion: 3,
+ serverCert: {
+ getBase64DERString() {
+ // This is the same test certificate as in
+ // test_security-info-certificate.js for consistency.
+ return "MIIDrDCCApSgAwIBAgIQCssoukZe5TkIdnRw883GEjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaMEwxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJjAkBgNVBAMTHURpZ2lDZXJ0IEVDQyBTZWN1cmUgU2VydmVyIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ghC6nfYJN6gLGSkE85AnCNyqQIKDjc/ITa4jVMU9tWRlUvzlgKNcR7E2Munn17voOZ/WpIRllNv68DLP679Wz9HJOeaBy6Wvqgvu1cYr3GkvXg6HuhbPGtkESvMNCuMo4IBITCCAR0wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAENjA0MDIGBFUdIAAwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAdBgNVHQ4EFgQUo53mH/naOU/AbuiRy5Wl2jHiCp8wHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEMBQADggEBAMeKoENL7HTJxavVHzA1Nm6YVntIrAVjrnuaVyRXzG/63qttnMe2uuzO58pzZNvfBDcKAEmzP58mrZGMIOgfiA4q+2Y3yDDo0sIkp0VILeoBUEoxlBPfjV/aKrtJPGHzecicZpIalir0ezZYoyxBEHQa0+1IttK7igZFcTMQMHp6mCHdJLnsnLWSB62DxsRq+HfmNb4TDydkskO/g+l3VtsIh5RHFPVfKK+jaEyDj2D3loB5hWp2Jp2VDCADjT7ueihlZGak2YPqmXTNbk19HOuNssWvFhtOyPNV6og4ETQdEa8/B6hPatJ0ES8q/HO3X8IVQwVs1n3aAr0im0/T+Xc=";
+ },
+ },
+};
+
+const MockHttpInfo = {
+ hostname: "include-subdomains.pinning.example.com",
+ private: false,
+};
+
+add_task(async function run_test() {
+ Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 1);
+ const result = await NetworkHelper.parseSecurityInfo(
+ MockSecurityInfo,
+ {},
+ MockHttpInfo,
+ new Map()
+ );
+ equal(result.hpkp, true, "Static HPKP detected.");
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/test_security-info-weakness-reasons.js b/devtools/shared/network-observer/test/xpcshell/test_security-info-weakness-reasons.js
new file mode 100644
index 0000000000..71d7675284
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_security-info-weakness-reasons.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that NetworkHelper.getReasonsForWeakness returns correct reasons for
+// weak requests.
+
+const wpl = Ci.nsIWebProgressListener;
+const TEST_CASES = [
+ {
+ description: "weak cipher",
+ input: wpl.STATE_IS_BROKEN | wpl.STATE_USES_WEAK_CRYPTO,
+ expected: ["cipher"],
+ },
+ {
+ description: "only STATE_IS_BROKEN flag",
+ input: wpl.STATE_IS_BROKEN,
+ expected: [],
+ },
+ {
+ description: "only STATE_IS_SECURE flag",
+ input: wpl.STATE_IS_SECURE,
+ expected: [],
+ },
+];
+
+function run_test() {
+ info("Testing NetworkHelper.getReasonsForWeakness.");
+
+ for (const { description, input, expected } of TEST_CASES) {
+ info("Testing " + description);
+
+ deepEqual(
+ NetworkHelper.getReasonsForWeakness(input),
+ expected,
+ "Got the expected reasons for weakness."
+ );
+ }
+}
diff --git a/devtools/shared/network-observer/test/xpcshell/test_throttle.js b/devtools/shared/network-observer/test/xpcshell/test_throttle.js
new file mode 100644
index 0000000000..5f8ef589fb
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/test_throttle.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-disable mozilla/use-chromeutils-generateqi */
+
+const { NetworkThrottleManager } = ChromeUtils.importESModule(
+ "resource://devtools/shared/network-observer/NetworkThrottleManager.sys.mjs"
+);
+const nsIScriptableInputStream = Ci.nsIScriptableInputStream;
+
+function TestStreamListener() {
+ this.state = "initial";
+}
+TestStreamListener.prototype = {
+ onStartRequest() {
+ this.setState("start");
+ },
+
+ onStopRequest() {
+ this.setState("stop");
+ },
+
+ onDataAvailable(request, inputStream, offset, count) {
+ const sin = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ nsIScriptableInputStream
+ );
+ sin.init(inputStream);
+ this.data = sin.read(count);
+ this.setState("data");
+ },
+
+ setState(state) {
+ this.state = state;
+ if (this._deferred) {
+ this._deferred.resolve(state);
+ this._deferred = null;
+ }
+ },
+
+ onStateChanged() {
+ if (!this._deferred) {
+ let resolve, reject;
+ const promise = new Promise(function (res, rej) {
+ resolve = res;
+ reject = rej;
+ });
+ this._deferred = { resolve, reject, promise };
+ }
+ return this._deferred.promise;
+ },
+};
+
+function TestChannel() {
+ this.state = "initial";
+ this.testListener = new TestStreamListener();
+ this._throttleQueue = null;
+}
+TestChannel.prototype = {
+ QueryInterface() {
+ return this;
+ },
+
+ get throttleQueue() {
+ return this._throttleQueue;
+ },
+
+ set throttleQueue(q) {
+ this._throttleQueue = q;
+ this.state = "throttled";
+ },
+
+ setNewListener(listener) {
+ this.listener = listener;
+ this.state = "listener";
+ return this.testListener;
+ },
+};
+
+add_task(async function () {
+ const throttler = new NetworkThrottleManager({
+ latencyMean: 1,
+ latencyMax: 1,
+ downloadBPSMean: 500,
+ downloadBPSMax: 500,
+ uploadBPSMean: 500,
+ uploadBPSMax: 500,
+ });
+
+ const uploadChannel = new TestChannel();
+ throttler.manageUpload(uploadChannel);
+ equal(
+ uploadChannel.state,
+ "throttled",
+ "NetworkThrottleManager set throttleQueue"
+ );
+
+ const downloadChannel = new TestChannel();
+ const testListener = downloadChannel.testListener;
+
+ const listener = throttler.manage(downloadChannel);
+ equal(
+ downloadChannel.state,
+ "listener",
+ "NetworkThrottleManager called setNewListener"
+ );
+
+ equal(testListener.state, "initial", "test listener in initial state");
+
+ // This method must be passed through immediately.
+ listener.onStartRequest(null);
+ equal(testListener.state, "start", "test listener started");
+
+ const TEST_INPUT = "hi bob";
+
+ const testStream = Cc["@mozilla.org/storagestream;1"].createInstance(
+ Ci.nsIStorageStream
+ );
+ testStream.init(512, 512);
+ const out = testStream.getOutputStream(0);
+ out.write(TEST_INPUT, TEST_INPUT.length);
+ out.close();
+ const testInputStream = testStream.newInputStream(0);
+
+ const activityDistributor = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ let activitySeen = false;
+ listener.addActivityCallback(
+ () => {
+ activitySeen = true;
+ },
+ null,
+ null,
+ null,
+ activityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE,
+ null,
+ TEST_INPUT.length,
+ null
+ );
+
+ // onDataAvailable is required to immediately read the data.
+ listener.onDataAvailable(null, testInputStream, 0, 6);
+ equal(testInputStream.available(), 0, "no more data should be available");
+ equal(
+ testListener.state,
+ "start",
+ "test listener should not have received data"
+ );
+ equal(activitySeen, false, "activity not distributed yet");
+
+ let newState = await testListener.onStateChanged();
+ equal(newState, "data", "test listener received data");
+ equal(testListener.data, TEST_INPUT, "test listener received all the data");
+ equal(activitySeen, true, "activity has been distributed");
+
+ const onChange = testListener.onStateChanged();
+ listener.onStopRequest(null, null);
+ newState = await onChange;
+ equal(newState, "stop", "onStateChanged reported");
+});
diff --git a/devtools/shared/network-observer/test/xpcshell/xpcshell.toml b/devtools/shared/network-observer/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..d10f08ac82
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/xpcshell.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = "devtools"
+head = "head.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+support-files = ""
+
+["test_network_helper.js"]
+
+["test_security-info-certificate.js"]
+
+["test_security-info-parser.js"]
+
+["test_security-info-protocol-version.js"]
+
+["test_security-info-state.js"]
+
+["test_security-info-static-hpkp.js"]
+
+["test_security-info-weakness-reasons.js"]
+
+["test_throttle.js"]