diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/shared/network-observer/test | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/shared/network-observer/test')
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] |