summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/mochitest/head_webrequest.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/mochitest/head_webrequest.js')
-rw-r--r--toolkit/components/extensions/test/mochitest/head_webrequest.js481
1 files changed, 481 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/mochitest/head_webrequest.js b/toolkit/components/extensions/test/mochitest/head_webrequest.js
new file mode 100644
index 0000000000..9e6b5cc910
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js
@@ -0,0 +1,481 @@
+"use strict";
+
+let commonEvents = {
+ onBeforeRequest: [{ urls: ["<all_urls>"] }, ["blocking"]],
+ onBeforeSendHeaders: [
+ { urls: ["<all_urls>"] },
+ ["blocking", "requestHeaders"],
+ ],
+ onSendHeaders: [{ urls: ["<all_urls>"] }, ["requestHeaders"]],
+ onBeforeRedirect: [{ urls: ["<all_urls>"] }],
+ onHeadersReceived: [
+ { urls: ["<all_urls>"] },
+ ["blocking", "responseHeaders"],
+ ],
+ // Auth tests will need to set their own events object
+ // "onAuthRequired": [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]],
+ onResponseStarted: [{ urls: ["<all_urls>"] }],
+ onCompleted: [{ urls: ["<all_urls>"] }, ["responseHeaders"]],
+ onErrorOccurred: [{ urls: ["<all_urls>"] }],
+};
+
+function background(events) {
+ const IP_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
+
+ let expect;
+ let ignore;
+ let defaultOrigin;
+ let watchAuth = Object.keys(events).includes("onAuthRequired");
+ let expectedIp = null;
+
+ browser.test.onMessage.addListener((msg, expected) => {
+ if (msg !== "set-expected") {
+ return;
+ }
+ expect = expected.expect;
+ defaultOrigin = expected.origin;
+ ignore = expected.ignore;
+ let promises = [];
+ // Initialize some stuff we'll need in the tests.
+ for (let entry of Object.values(expect)) {
+ // a place for the test infrastructure to store some state.
+ entry.test = {};
+ // Each entry in expected gets a Promise that will be resolved in the
+ // last event for that entry. This will either be onCompleted, or the
+ // last entry if an events list was provided.
+ promises.push(
+ new Promise(resolve => {
+ entry.test.resolve = resolve;
+ })
+ );
+ // If events was left undefined, we're expecting all normal events we're
+ // listening for, exclude onBeforeRedirect and onErrorOccurred
+ if (entry.events === undefined) {
+ entry.events = Object.keys(events).filter(
+ name => name != "onErrorOccurred" && name != "onBeforeRedirect"
+ );
+ }
+ if (entry.optional_events === undefined) {
+ entry.optional_events = [];
+ }
+ }
+ // When every expected entry has finished our test is done.
+ Promise.all(promises).then(() => {
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("continue");
+ });
+
+ // Retrieve the per-file/test expected values.
+ function getExpected(details) {
+ let url = new URL(details.url);
+ let filename = url.pathname.split("/").pop();
+ if (ignore && ignore.includes(filename)) {
+ return;
+ }
+ let expected = expect[filename];
+ if (!expected) {
+ browser.test.fail(`unexpected request ${filename}`);
+ return;
+ }
+ // Save filename for redirect verification.
+ expected.test.filename = filename;
+ return expected;
+ }
+
+ // Process any test header modifications that can happen in request or response phases.
+ // If a test includes headers, it needs a complete header object, no undefined
+ // objects even if empty:
+ // request: {
+ // add: {"HeaderName": "value",},
+ // modify: {"HeaderName": "value",},
+ // remove: ["HeaderName",],
+ // },
+ // response: {
+ // add: {"HeaderName": "value",},
+ // modify: {"HeaderName": "value",},
+ // remove: ["HeaderName",],
+ // },
+ function processHeaders(phase, expected, details) {
+ // This should only happen once per phase [request|response].
+ browser.test.assertFalse(
+ !!expected.test[phase],
+ `First processing of headers for ${phase}`
+ );
+ expected.test[phase] = true;
+
+ let headers = details[`${phase}Headers`];
+ browser.test.assertTrue(
+ Array.isArray(headers),
+ `${phase}Headers array present`
+ );
+
+ let { add, modify, remove } = expected.headers[phase];
+
+ for (let name in add) {
+ browser.test.assertTrue(
+ !headers.find(h => h.name === name),
+ `header ${name} to be added not present yet in ${phase}Headers`
+ );
+ let header = { name: name };
+ if (name.endsWith("-binary")) {
+ header.binaryValue = Array.from(add[name], c => c.charCodeAt(0));
+ } else {
+ header.value = add[name];
+ }
+ headers.push(header);
+ }
+
+ let modifiedAny = false;
+ for (let header of headers) {
+ if (header.name.toLowerCase() in modify) {
+ header.value = modify[header.name.toLowerCase()];
+ modifiedAny = true;
+ }
+ }
+ browser.test.assertTrue(
+ modifiedAny,
+ `at least one ${phase}Headers element to modify`
+ );
+
+ let deletedAny = false;
+ for (let j = headers.length; j-- > 0; ) {
+ if (remove.includes(headers[j].name.toLowerCase())) {
+ headers.splice(j, 1);
+ deletedAny = true;
+ }
+ }
+ browser.test.assertTrue(
+ deletedAny,
+ `at least one ${phase}Headers element to delete`
+ );
+
+ return headers;
+ }
+
+ // phase is request or response.
+ function checkHeaders(phase, expected, details) {
+ if (!/^https?:/.test(details.url)) {
+ return;
+ }
+
+ let headers = details[`${phase}Headers`];
+ browser.test.assertTrue(
+ Array.isArray(headers),
+ `valid ${phase}Headers array`
+ );
+
+ let { add, modify, remove } = expected.headers[phase];
+ for (let name in add) {
+ let value = headers.find(
+ h => h.name.toLowerCase() === name.toLowerCase()
+ ).value;
+ browser.test.assertEq(
+ value,
+ add[name],
+ `header ${name} correctly injected in ${phase}Headers`
+ );
+ }
+
+ for (let name in modify) {
+ let value = headers.find(
+ h => h.name.toLowerCase() === name.toLowerCase()
+ ).value;
+ browser.test.assertEq(
+ value,
+ modify[name],
+ `header ${name} matches modified value`
+ );
+ }
+
+ for (let name of remove) {
+ let found = headers.find(
+ h => h.name.toLowerCase() === name.toLowerCase()
+ );
+ browser.test.assertFalse(
+ !!found,
+ `deleted header ${name} still found in ${phase}Headers`
+ );
+ }
+ }
+
+ let listeners = {
+ onBeforeRequest(expected, details, result) {
+ // Save some values to test request consistency in later events.
+ browser.test.assertTrue(
+ details.tabId !== undefined,
+ `tabId ${details.tabId}`
+ );
+ browser.test.assertTrue(
+ details.requestId !== undefined,
+ `requestId ${details.requestId}`
+ );
+ // Validate requestId if it's already set, this happens with redirects.
+ if (expected.test.requestId !== undefined) {
+ browser.test.assertEq(
+ "string",
+ typeof expected.test.requestId,
+ `requestid ${expected.test.requestId} is string`
+ );
+ browser.test.assertEq(
+ "string",
+ typeof details.requestId,
+ `requestid ${details.requestId} is string`
+ );
+ browser.test.assertEq(
+ "number",
+ typeof parseInt(details.requestId, 10),
+ "parsed requestid is number"
+ );
+ browser.test.assertEq(
+ expected.test.requestId,
+ details.requestId,
+ "redirects will keep the same requestId"
+ );
+ } else {
+ // Save any values we want to validate in later events.
+ expected.test.requestId = details.requestId;
+ expected.test.tabId = details.tabId;
+ }
+ // Tests we don't need to do every event.
+ browser.test.assertTrue(
+ details.type.toUpperCase() in browser.webRequest.ResourceType,
+ `valid resource type ${details.type}`
+ );
+ if (details.type == "main_frame") {
+ browser.test.assertEq(
+ 0,
+ details.frameId,
+ "frameId is zero when type is main_frame, see bug 1329299"
+ );
+ }
+ },
+ onBeforeSendHeaders(expected, details, result) {
+ if (expected.headers && expected.headers.request) {
+ result.requestHeaders = processHeaders("request", expected, details);
+ }
+ if (expected.redirect) {
+ browser.test.log(`${name} redirect request`);
+ result.redirectUrl = details.url.replace(
+ expected.test.filename,
+ expected.redirect
+ );
+ }
+ },
+ onBeforeRedirect() {},
+ onSendHeaders(expected, details, result) {
+ if (expected.headers && expected.headers.request) {
+ checkHeaders("request", expected, details);
+ }
+ },
+ onResponseStarted() {},
+ onHeadersReceived(expected, details, result) {
+ let expectedStatus = expected.status || 200;
+ // If authentication is being requested we don't fail on the status code.
+ if (watchAuth && [401, 407].includes(details.statusCode)) {
+ expectedStatus = details.statusCode;
+ }
+ browser.test.assertEq(
+ expectedStatus,
+ details.statusCode,
+ `expected HTTP status received for ${details.url} ${details.statusLine}`
+ );
+ if (expected.headers && expected.headers.response) {
+ result.responseHeaders = processHeaders("response", expected, details);
+ }
+ },
+ onAuthRequired(expected, details, result) {
+ result.authCredentials = expected.authInfo;
+ },
+ onCompleted(expected, details, result) {
+ // If we have already completed a GET request for this url,
+ // and it was found, we expect for the response to come fromCache.
+ // expected.cached may be undefined, force boolean.
+ if (typeof expected.cached === "boolean") {
+ let expectCached =
+ expected.cached &&
+ details.method === "GET" &&
+ details.statusCode != 404;
+ browser.test.assertEq(
+ expectCached,
+ details.fromCache,
+ "fromCache is correct"
+ );
+ }
+ // We can only tell IPs for non-cached HTTP requests.
+ if (!details.fromCache && /^https?:/.test(details.url)) {
+ browser.test.assertTrue(
+ IP_PATTERN.test(details.ip),
+ `IP for ${details.url} looks IP-ish: ${details.ip}`
+ );
+
+ // We can't easily predict the IP ahead of time, so just make
+ // sure they're all consistent.
+ expectedIp = expectedIp || details.ip;
+ browser.test.assertEq(
+ expectedIp,
+ details.ip,
+ `correct ip for ${details.url}`
+ );
+ }
+ if (expected.headers && expected.headers.response) {
+ checkHeaders("response", expected, details);
+ }
+ },
+ onErrorOccurred(expected, details, result) {
+ if (expected.error) {
+ if (Array.isArray(expected.error)) {
+ browser.test.assertTrue(
+ expected.error.includes(details.error),
+ "expected error message received in onErrorOccurred"
+ );
+ } else {
+ browser.test.assertEq(
+ expected.error,
+ details.error,
+ "expected error message received in onErrorOccurred"
+ );
+ }
+ }
+ },
+ };
+
+ function getListener(name) {
+ return details => {
+ let result = {};
+ browser.test.log(`${name} ${details.requestId} ${details.url}`);
+ let expected = getExpected(details);
+ if (!expected) {
+ return result;
+ }
+ let expectedEvent = expected.events[0] == name;
+ if (expectedEvent) {
+ expected.events.shift();
+ } else {
+ // e10s vs. non-e10s errors can end with either onCompleted or onErrorOccurred
+ expectedEvent = expected.optional_events.includes(name);
+ }
+ browser.test.assertTrue(expectedEvent, `received ${name}`);
+ browser.test.assertEq(
+ expected.type,
+ details.type,
+ "resource type is correct"
+ );
+ browser.test.assertEq(
+ expected.origin || defaultOrigin,
+ details.originUrl,
+ "origin is correct"
+ );
+
+ if (name != "onBeforeRequest") {
+ // On events after onBeforeRequest, check the previous values.
+ browser.test.assertEq(
+ expected.test.requestId,
+ details.requestId,
+ "correct requestId"
+ );
+ browser.test.assertEq(
+ expected.test.tabId,
+ details.tabId,
+ "correct tabId"
+ );
+ }
+ try {
+ listeners[name](expected, details, result);
+ } catch (e) {
+ browser.test.fail(`unexpected webrequest failure ${name} ${e}`);
+ }
+
+ if (expected.cancel && expected.cancel == name) {
+ browser.test.log(`${name} cancel request`);
+ browser.test.sendMessage("cancelled");
+ result.cancel = true;
+ }
+ // If we've used up all the events for this test, resolve the promise.
+ // If something wrong happens and more events come through, there will be
+ // failures.
+ if (expected.events.length <= 0) {
+ expected.test.resolve();
+ }
+ return result;
+ };
+ }
+
+ for (let [name, args] of Object.entries(events)) {
+ browser.test.log(`adding listener for ${name}`);
+ try {
+ browser.webRequest[name].addListener(getListener(name), ...args);
+ } catch (e) {
+ browser.test.assertTrue(
+ /\brequestBody\b/.test(e.message),
+ "Request body is unsupported"
+ );
+
+ // RequestBody is disabled in release builds.
+ if (!/\brequestBody\b/.test(e.message)) {
+ throw e;
+ }
+
+ args.splice(args.indexOf("requestBody"), 1);
+ browser.webRequest[name].addListener(getListener(name), ...args);
+ }
+ }
+}
+
+/* exported makeExtension */
+
+function makeExtension(events = commonEvents) {
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+ background: `(${background})(${JSON.stringify(events)})`,
+ });
+}
+
+/* exported addStylesheet */
+
+function addStylesheet(file) {
+ let link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("href", file);
+ document.body.appendChild(link);
+}
+
+/* exported addLink */
+
+function addLink(file) {
+ let a = document.createElement("a");
+ a.setAttribute("href", file);
+ a.setAttribute("target", "_blank");
+ a.setAttribute("rel", "opener");
+ document.body.appendChild(a);
+ return a;
+}
+
+/* exported addImage */
+
+function addImage(file) {
+ let img = document.createElement("img");
+ img.setAttribute("src", file);
+ document.body.appendChild(img);
+}
+
+/* exported addScript */
+
+function addScript(file) {
+ let script = document.createElement("script");
+ script.setAttribute("type", "text/javascript");
+ script.setAttribute("src", file);
+ document.getElementsByTagName("head").item(0).appendChild(script);
+}
+
+/* exported addFrame */
+
+function addFrame(file) {
+ let frame = document.createElement("iframe");
+ frame.setAttribute("width", "200");
+ frame.setAttribute("height", "200");
+ frame.setAttribute("src", file);
+ document.body.appendChild(frame);
+}