summaryrefslogtreecommitdiffstats
path: root/devtools/shared/network-observer/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /devtools/shared/network-observer/test
parentInitial commit. (diff)
downloadfirefox-esr-upstream/115.8.0esr.tar.xz
firefox-esr-upstream/115.8.0esr.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/network-observer/test')
-rw-r--r--devtools/shared/network-observer/test/browser/browser.ini15
-rw-r--r--devtools/shared/network-observer/test/browser/browser_networkobserver.js72
-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/doc_network-observer.html25
-rw-r--r--devtools/shared/network-observer/test/browser/gzipped.sjs44
-rw-r--r--devtools/shared/network-observer/test/browser/head.js92
-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/sjs_network-observer-test-server.sjs198
-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.ini15
20 files changed, 1341 insertions, 0 deletions
diff --git a/devtools/shared/network-observer/test/browser/browser.ini b/devtools/shared/network-observer/test/browser/browser.ini
new file mode 100644
index 0000000000..f1d09d2c1b
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ doc_network-observer.html
+ gzipped.sjs
+ override.html
+ override.js
+ sjs_network-observer-test-server.sjs
+
+[browser_networkobserver.js]
+skip-if = http3 # Bug 1829298
+[browser_networkobserver_invalid_constructor.js]
+[browser_networkobserver_override.js]
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..73e9f8510a
--- /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.sendRequest(_url);
+ });
+
+ const eventsCount = await onNetworkEvents;
+ is(eventsCount, 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.sendRequest(_url);
+ }
+ }
+ );
+
+ const eventsCount = await onNetworkEvents;
+ is(
+ eventsCount,
+ 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.sendRequest(_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_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/doc_network-observer.html b/devtools/shared/network-observer/test/browser/doc_network-observer.html
new file mode 100644
index 0000000000..90cd9be69b
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/doc_network-observer.html
@@ -0,0 +1,25 @@
+<!-- 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">
+ "use strict";
+
+ window.sendRequest = url => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.send(null);
+ }
+ </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..ab03163d9d
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/head.js
@@ -0,0 +1,92 @@
+/* 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.loadURIString(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;
+}
+
+/**
+ * Create a simple network event owner, with empty implementations of all
+ * the expected APIs for a NetworkEventOwner.
+ */
+function createNetworkEventOwner(event) {
+ return {
+ addEventTimings: () => {},
+ addResponseCache: () => {},
+ addResponseContent: () => {},
+ addResponseStart: () => {},
+ addSecurityInfo: () => {},
+ addServerTimings: () => {},
+ };
+}
+
+/**
+ * Wait for network events matching the provided URL, until the count reaches
+ * the provided expected count.
+ *
+ * @param {string} expectedUrl
+ * The URL which should be monitored by the NetworkObserver.
+ * @param {number} expectedRequestsCount
+ * How many different events (requests) are expected.
+ * @returns {Promise}
+ * A promise which will resolve with the current count, when the expected
+ * count is reached.
+ */
+async function waitForNetworkEvents(expectedUrl, expectedRequestsCount) {
+ let eventsCount = 0;
+ const networkObserver = new NetworkObserver({
+ ignoreChannelFunction: channel => channel.URI.spec !== expectedUrl,
+ onNetworkEvent: event => {
+ info("waitForNetworkEvents received a new event");
+ eventsCount++;
+ return createNetworkEventOwner(event);
+ },
+ });
+ registerCleanupFunction(() => networkObserver.destroy());
+
+ info("Wait until the events count reaches " + expectedRequestsCount);
+ await BrowserTestUtils.waitForCondition(
+ () => eventsCount >= expectedRequestsCount
+ );
+ return eventsCount;
+}
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/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..918d6e8447
--- /dev/null
+++ b/devtools/shared/network-observer/test/browser/sjs_network-observer-test-server.sjs
@@ -0,0 +1,198 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.importGlobalProperties(["TextEncoder"]);
+
+// 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.ini b/devtools/shared/network-observer/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..3f5c1b674a
--- /dev/null
+++ b/devtools/shared/network-observer/test/xpcshell/xpcshell.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+firefox-appdir = browser
+skip-if = toolkit == '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]