Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: os == 'win' && socketprocess_networking && fission OR os == 'mac' && socketprocess_networking && fission OR os == 'mac' && debug OR os == 'linux' && socketprocess_networking
- Manifest: toolkit/components/extensions/test/xpcshell/xpcshell-remote.toml includes toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
- Manifest: toolkit/components/extensions/test/xpcshell/xpcshell.toml includes toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
"use strict";
// This test file verifies that the declarativeNetRequest API can modify
// network requests as expected without the presence of the webRequest API. See
// test_ext_dnr_webRequest.js for the interaction between webRequest and DNR.
add_setup(() => {
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
Services.prefs.setBoolPref("extensions.dnr.enabled", true);
});
const server = createHttpServer({
hosts: ["example.com", "example.net", "example.org", "redir", "dummy"],
});
server.registerPathHandler("/cors_202", (req, res) => {
res.setStatusLine(req.httpVersion, 202, "Accepted");
// The extensions in this test have minimal permissions, so grant CORS to
// allow them to read the response without host permissions.
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Max-Age", "0");
res.write("cors_response");
});
server.registerPathHandler("/never_reached", (req, res) => {
Assert.ok(false, "Server should never have been reached");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Max-Age", "0");
});
let gPreflightCount = 0;
server.registerPathHandler("/preflight_count", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Max-Age", "0");
res.setHeader("Access-Control-Allow-Methods", "NONSIMPLE");
if (req.method === "OPTIONS") {
++gPreflightCount;
} else {
// CORS Preflight considers 2xx to be successful. To rule out inadvertent
// server opt-in to CORS, respond with a non-2xx response.
res.setStatusLine(req.httpVersion, 418, "I'm a teapot");
res.write(`count=${gPreflightCount}`);
}
});
server.registerPathHandler("/", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Max-Age", "0");
res.write("Dummy page");
});
async function contentFetch(initiatorURL, url, options) {
let contentPage = await ExtensionTestUtils.loadContentPage(initiatorURL);
// Sanity check: that the initiator is as specified, and not redirected.
Assert.equal(
await contentPage.spawn([], () => content.document.URL),
initiatorURL,
`Expected document load at: ${initiatorURL}`
);
let result = await contentPage.spawn([{ url, options }], async args => {
try {
let req = await content.fetch(args.url, args.options);
return {
status: req.status,
url: req.url,
body: await req.text(),
};
} catch (e) {
return { error: e.message };
}
});
await contentPage.close();
return result;
}
async function checkCanFetchFromOtherExtension() {
let otherExtension = ExtensionTestUtils.loadExtension({
async background() {
browser.test.assertEq(202, req.status, "not blocked by other extension");
browser.test.assertEq("cors_response", await req.text());
browser.test.sendMessage("other_extension_done");
},
});
await otherExtension.startup();
await otherExtension.awaitMessage("other_extension_done");
await otherExtension.unload();
}
add_task(async function block_request_with_dnr() {
async function background() {
await browser.declarativeNetRequest.updateSessionRules({
addRules: [
{
id: 1,
condition: { requestMethods: ["get"] },
action: { type: "block" },
},
{
id: 2,
condition: { requestMethods: ["head"] },
action: { type: "allow" },
},
],
});
{
// Request not matching DNR.
browser.test.assertEq(202, req.status, "allowed without DNR rule");
browser.test.assertEq("cors_response", await req.text());
}
{
// Request with "allow" DNR action.
browser.test.assertEq(202, req.status, "allowed by DNR rule");
browser.test.assertEq("", await req.text(), "no response for HEAD");
}
// Request with "block" DNR action.
await browser.test.assertRejects(
"NetworkError when attempting to fetch resource.",
"blocked by DNR rule"
);
browser.test.sendMessage("tested_dnr_block");
}
let extension = ExtensionTestUtils.loadExtension({
allowInsecureRequests: true,
background,
manifest: {
manifest_version: 3,
permissions: ["declarativeNetRequest"],
},
});
await extension.startup();
await extension.awaitMessage("tested_dnr_block");
// DNR should not only work with requests within the extension, but also from
// web pages.
Assert.deepEqual(
{ error: "NetworkError when attempting to fetch resource." },
"Blocked by DNR with declarativeNetRequestWithHostAccess"
);
// declarativeNetRequest does not allow extensions to block requests from
// other extensions.
await checkCanFetchFromOtherExtension();
// Except when the user opts in via a preference. When the pref is on, then:
// The declarativeNetRequest permission grants the ability to block requests
// from other extensions. (The declarativeNetRequestWithHostAccess permission
// does not; see test task block_with_declarativeNetRequestWithHostAccess.)
let otherExtension = ExtensionTestUtils.loadExtension({
async background() {
await browser.test.assertRejects(
"NetworkError when attempting to fetch resource.",
"blocked by different extension with declarativeNetRequest permission"
);
browser.test.sendMessage("other_extension_done");
},
});
await runWithPrefs(
[["extensions.dnr.match_requests_from_other_extensions", true]],
async () => {
info("Verifying that fetch() from extension is intercepted with pref");
await otherExtension.startup();
await otherExtension.awaitMessage("other_extension_done");
await otherExtension.unload();
}
);
await extension.unload();
});
// Verifies that the "declarativeNetRequestWithHostAccess" permission can only
// block if it has permission for the initiator.
add_task(async function block_with_declarativeNetRequestWithHostAccess() {
let extension = ExtensionTestUtils.loadExtension({
async background() {
await browser.declarativeNetRequest.updateSessionRules({
addRules: [{ id: 1, condition: {}, action: { type: "block" } }],
});
browser.test.sendMessage("dnr_registered");
},
temporarilyInstalled: true, // Needed for granted_host_permissions
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
granted_host_permissions: true,
host_permissions: ["<all_urls>"],
permissions: ["declarativeNetRequestWithHostAccess"],
},
});
await extension.startup();
await extension.awaitMessage("dnr_registered");
Assert.deepEqual(
{ error: "NetworkError when attempting to fetch resource." },
"Blocked by DNR with declarativeNetRequestWithHostAccess"
);
// Extensions cannot have permissions for another extension and therefore the
// DNR rule never applies.
await checkCanFetchFromOtherExtension();
// Sanity check: even with the pref, the declarativeNetRequestWithHostAccess
// permission should not grant access.
info("Verifying that access is not allowed, despite the pref being true");
await runWithPrefs(
[["extensions.dnr.match_requests_from_other_extensions", true]],
checkCanFetchFromOtherExtension
);
await extension.unload();
});
add_task(async function block_in_sandboxed_extension_page() {
const filesWithSandbox = {
"page_with_sandbox.html": `<!DOCTYPE html><meta charset="utf-8">
<script src="page_with_sandbox.js"></script>
<iframe src="sandbox.html" sandbox="allow-scripts"></iframe>
`,
"page_with_sandbox.js": () => {
// Sent by sandbox.js:
window.onmessage = e => {
browser.test.assertEq("null", e.origin, "Sender has opaque origin");
browser.test.sendMessage("fetch_result", e.data);
};
},
"sandbox.html": `<script src="sandbox.js"></script>`,
"sandbox.js": async () => {
try {
// Note that the test server responds with CORS headers, so we should
// be able to fetch this URL:
parent.postMessage("FETCH_ALLOWED", "*");
} catch (e) {
// The only way for this to fail in this test is when DNR blocks it.
parent.postMessage("FETCH_BLOCKED", "*");
}
},
};
async function checkFetchInSandboxedExtensionPage(ext) {
let contentPage = await ExtensionTestUtils.loadContentPage(
);
let result = await ext.awaitMessage("fetch_result");
await contentPage.close();
return result;
}
let extension = ExtensionTestUtils.loadExtension({
async background() {
await browser.declarativeNetRequest.updateSessionRules({
addRules: [{ id: 1, condition: {}, action: { type: "block" } }],
});
browser.test.sendMessage("dnr_registered");
},
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
permissions: ["declarativeNetRequest"],
},
files: filesWithSandbox,
});
await extension.startup();
await extension.awaitMessage("dnr_registered");
Assert.equal(
await checkFetchInSandboxedExtensionPage(extension),
"FETCH_BLOCKED",
"DNR blocks request from sandboxed page in own extension"
);
let otherExtension = ExtensionTestUtils.loadExtension({
files: filesWithSandbox,
});
await otherExtension.startup();
// Note: In Firefox, webRequest can intercept requests from opaque origins
// opened by other extensions. In contrast, that is not the case in Chrome.
Assert.equal(
await checkFetchInSandboxedExtensionPage(otherExtension),
"FETCH_BLOCKED",
"DNR can block request from sandboxed page in other extension"
);
await runWithPrefs(
[["extensions.dnr.match_requests_from_other_extensions", true]],
async () => {
info("Verifying that fetch() from extension sandbox is matched via pref");
Assert.equal(
await checkFetchInSandboxedExtensionPage(otherExtension),
"FETCH_BLOCKED",
"DNR can block request from sandboxed page in other extension via pref"
);
}
);
await extension.unload();
// As a sanity check, to verify that the tests above do not always return
// FETCH_BLOCKED, run a test case that returns FETCH_ALLOWED:
Assert.equal(
await checkFetchInSandboxedExtensionPage(otherExtension),
"FETCH_ALLOWED",
"DNR does not affect sandboxed extensions after unloading the DNR extension"
);
await otherExtension.unload();
});
// Verifies that upgradeScheme works.
// test we just verify whether the upgrade has been attempted. Coverage that
// verifies that the upgraded request completes is in:
// toolkit/components/extensions/test/mochitest/test_ext_dnr_upgradeScheme.html
add_task(async function upgradeScheme_declarativeNetRequestWithHostAccess() {
let extension = ExtensionTestUtils.loadExtension({
async background() {
await browser.declarativeNetRequest.updateSessionRules({
addRules: [
{
id: 1,
condition: { excludedRequestDomains: ["dummy"] },
action: { type: "upgradeScheme" },
},
{
id: 2,
// As a work-around, we just redirect the https:-request to http.
condition: { urlFilter: "|https:*" },
},
// The upgradeScheme and redirect actions have equal precedence. To
// make sure that the redirect action is executed when both rules
// match, we assign a higher priority to the redirect action.
priority: 2,
},
],
});
browser.test.assertEq(
req.url,
"upgradeScheme upgraded to https"
);
browser.test.assertEq("cors_response", await req.text());
browser.test.sendMessage("tested_dnr_upgradeScheme");
},
temporarilyInstalled: true, // Needed for granted_host_permissions.
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
granted_host_permissions: true,
host_permissions: ["*://dummy/*", "*://redir/*"],
permissions: ["declarativeNetRequestWithHostAccess"],
},
});
await extension.startup();
await extension.awaitMessage("tested_dnr_upgradeScheme");
// Request to same-origin subresource, which should be upgraded.
Assert.equal(
"upgradeScheme + host access should upgrade (same-origin request)"
);
// Request to cross-origin subresource, which should be upgraded.
// Note: after the upgrade, a cross-origin redirect happens. Internally, we
// reflect the Origin request header in the Access-Control-Allow-Origin (ACAO)
// response header, to ensure that the request is accepted by CORS. See
Assert.equal(
"upgradeScheme + host access should upgrade (cross-origin request)"
);
// The DNR extension does not have example.net in host_permissions.
Assert.equal(
urlNoHostPerms,
"upgradeScheme not matched when extension lacks host access"
);
await extension.unload();
});
add_task(async function redirect_request_with_dnr() {
async function background() {
await browser.declarativeNetRequest.updateSessionRules({
addRules: [
{
id: 1,
condition: {
requestDomains: ["example.com"],
requestMethods: ["get"],
},
},
},
{
id: 2,
// Note: extension does not have example.org host permission.
condition: { requestDomains: ["example.org"] },
},
},
],
});
// The extension only has example.com permission, but the redirects to
// example.net are still due to the CORS headers from the server.
{
// Simple GET request.
browser.test.assertEq(202, req.status, "redirected by DNR (simple)");
browser.test.assertEq("cors_response", await req.text());
}
{
// GeT request should be matched despite having a different case.
method: "GeT",
});
browser.test.assertEq(202, req.status, "redirected by DNR (GeT)");
browser.test.assertEq("cors_response", await req.text());
}
{
// Host permission missing for request, request not redirected by DNR.
// Response is readable due to the CORS response headers from the server.
browser.test.assertEq(202, req.status, "not redirected by DNR");
browser.test.assertEq("cors_response", await req.text());
}
browser.test.notifyPass();
}
let extension = ExtensionTestUtils.loadExtension({
background,
temporarilyInstalled: true, // Needed for granted_host_permissions
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
granted_host_permissions: true,
host_permissions: ["*://example.com/*"],
permissions: ["declarativeNetRequest"],
},
});
await extension.startup();
await extension.awaitFinish();
let otherExtension = ExtensionTestUtils.loadExtension({
async background() {
// The DNR extension has permissions for example.com, but not for this
// extension. Therefore the "redirect" action should not apply.
browser.test.assertEq(202, req.status, "not redirected by DNR");
browser.test.assertEq("cors_response", await req.text());
browser.test.sendMessage("other_extension_done");
},
});
await otherExtension.startup();
await otherExtension.awaitMessage("other_extension_done");
await otherExtension.unload();
await extension.unload();
});
// Verifies that DNR redirects requiring a CORS preflight behave as expected.
add_task(async function redirect_request_with_dnr_cors_preflight() {
// Most other test tasks only test requests within the test extension. This
// test intentionally triggers requests outside the extension, to make sure
// that the usual CORS mechanisms is triggered (instead of exceptions from
// host permissions).
async function background() {
await browser.declarativeNetRequest.updateSessionRules({
addRules: [
{
id: 1,
condition: {
requestDomains: ["redir"],
excludedRequestMethods: ["options"],
},
},
},
{
id: 2,
condition: {
requestDomains: ["example.net"],
excludedRequestMethods: ["nonsimple"], // note: redirects "options"
},
},
},
],
});
method: "NONSIMPLE",
});
// Extension has permission for "redir", but not for the redirect target.
// The request is non-simple (see below for explanation of non-simple), so
// a preflight (OPTIONS) request to /preflight_count is expected before the
// redirection target is requested.
browser.test.assertEq(
"count=1",
await req.text(),
"Got preflight before redirect target because of missing host_permissions"
);
browser.test.sendMessage("continue_preflight_tests");
}
let extension = ExtensionTestUtils.loadExtension({
background,
temporarilyInstalled: true, // Needed for granted_host_permissions
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
granted_host_permissions: true,
// "redir" and "example.net" are needed to allow redirection of these.
host_permissions: ["*://redir/*", "*://example.net/*", "*://dummy/*"],
permissions: ["declarativeNetRequest"],
},
});
gPreflightCount = 0;
await extension.startup();
await extension.awaitMessage("continue_preflight_tests");
gPreflightCount = 0; // value already checked before continue_preflight_tests.
// Simple request (i.e. without preflight requirement), that's redirected to
// another URL by the DNR rule. The redirect should be accepted, and in
// particular not be blocked by the same-origin policy. The redirect target
// (/preflight_count) is readable due to the CORS headers from the server.
Assert.deepEqual(
// count=0: A simple request does not trigger a preflight (OPTIONS) request.
"Simple request should not have a preflight."
);
// Any request method other than "GET", "POST" and "PUT" (e.g "NONSIMPLE") is
// a non-simple request that triggers a preflight request ("OPTIONS").
//
// Usually, this happens (without extension-triggered redirects):
// 1. NONSIMPLE /never_reached : is started, but does NOT hit the server yet.
// 2. OPTIONS /never_reached + Access-Control-Request-Method: NONSIMPLE
// 3. NONSIMPLE /never_reached : reaches the server if allowed by OPTIONS.
//
// With an extension-initiated redirect to /preflight_count:
// 1. NONSIMPLE /never_reached : is started, but does not hit the server yet.
// 2. extension redirects to /preflight_count
// 3. OPTIONS /preflight_count + Access-Control-Request-Method: NONSIMPLE
// - This is because the redirect preserves the request method/body/etc.
// 4. NONSIMPLE /preflight_count : reaches the server if allowed by OPTIONS.
Assert.deepEqual(
method: "NONSIMPLE",
}),
// Due to excludedRequestMethods: ["options"], the preflight for the
// redirect target is not intercepted, so the server sees a preflight.
"Initial URL redirected, redirection target has preflight"
);
gPreflightCount = 0;
// The "example.net" rule has "excludedRequestMethods": ["nonsimple"], so the
// initial "NONSIMPLE" request is not immediately redirected. Therefore the
// preflight request happens. This OPTIONS request is matched by the DNR rule
// and redirected to /preflight_count. While preflight_count offers a very
// permissive preflight response, it is not even fetched:
// Only a 2xx HTTP status is considered a valid response to a pre-flight.
// A redirect is like a 3xx HTTP status, so the whole request is rejected,
// and the redirect is not followed for the OPTIONS request.
Assert.deepEqual(
method: "NONSIMPLE",
}),
{ error: "NetworkError when attempting to fetch resource." },
"Redirect of preflight request (OPTIONS) should be a CORS failure"
);
Assert.equal(gPreflightCount, 0, "Preflight OPTIONS has been intercepted");
await extension.unload();
});
// Tests that DNR redirect rules can be chained.
add_task(async function redirect_request_with_dnr_multiple_hops() {
async function background() {
// Set up redirects from example.com up until dummy.
let hosts = ["example.com", "example.net", "example.org", "redir", "dummy"];
let rules = [];
for (let i = 1; i < hosts.length; ++i) {
const from = hosts[i - 1];
const to = hosts[i];
const end = hosts.length - 1 === i;
rules.push({
id: i,
condition: { requestDomains: [from] },
action: {
type: "redirect",
redirect: {
// All intermediate redirects should never hit the server, but the
// last one should..
},
},
});
}
await browser.declarativeNetRequest.updateSessionRules({ addRules: rules });
browser.test.assertEq(200, req.status, "redirected by DNR (multiple)");
browser.test.assertEq("Dummy page", await req.text());
browser.test.notifyPass();
}
let extension = ExtensionTestUtils.loadExtension({
background,
temporarilyInstalled: true, // Needed for granted_host_permissions
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
granted_host_permissions: true,
host_permissions: ["*://*/*"], // matches all in the |hosts| list.
permissions: ["declarativeNetRequest"],
},
});
await extension.startup();
await extension.awaitFinish();
// Test again, but without special extension permissions to verify that DNR
// redirects pass CORS checks.
Assert.deepEqual(
"Multiple redirects by DNR, requested from web origin."
);
await extension.unload();
});
add_task(async function redirect_request_with_dnr_with_redirect_loop() {
async function background() {
await browser.declarativeNetRequest.updateSessionRules({
addRules: [
{
id: 1,
// requestMethods is mutually exclusive with the other rule.
condition: { regexFilter: "^(.+)$", requestMethods: ["post"] },
action: {
type: "redirect",
redirect: {
// Appends "?loop" to the request URL
regexSubstitution: "\\1?loop",
},
},
},
{
id: 2,
// requestMethods is mutually exclusive with the other rule.
condition: { requestDomains: ["redir"], requestMethods: ["get"] },
action: {
type: "redirect",
redirect: {
// Despite redirect.url matching the condition, the redirect loop
// should be caught because of the obvious fact that the URL did
// not change.
},
},
},
],
});
// Redirect where the redirect URL changes at every redirect.
await browser.test.assertRejects(
"NetworkError when attempting to fetch resource.",
"Redirect loop caught (redirect target differs at every redirect)"
);
async function assertRedirect(url, expected, description) {
// method: "get" could only match rule 2.
let res = await fetch(url);
browser.test.assertDeepEq(
expected,
{ status: res.status, url: res.url, redirected: res.redirected },
description
);
}
// Redirect with initially a different URL.
await assertRedirect(
"Redirect loop caught (initially different URL)"
);
// Redirect where redirect is exactly the same URL as requested.
await assertRedirect(
"Redirect loop caught (redirect target same as initial URL)"
);
browser.test.notifyPass();
}
let extension = ExtensionTestUtils.loadExtension({
background,
temporarilyInstalled: true, // Needed for granted_host_permissions
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
granted_host_permissions: true,
host_permissions: ["*://redir/*"],
permissions: ["declarativeNetRequest"],
},
});
await extension.startup();
await extension.awaitFinish();
await extension.unload();
});
// Tests that redirect to extensionPath works, provided that the initiator is
// either the extension itself, or in host_permissions. Moreover, the requested
// resource must match a web_accessible_resources entry for both the initiator
// AND the pre-redirect URL.
add_task(async function redirect_request_with_dnr_to_extensionPath() {
async function background() {
await browser.declarativeNetRequest.updateSessionRules({
addRules: [
{
id: 1,
condition: { requestDomains: ["redir"], requestMethods: ["post"] },
action: {
type: "redirect",
redirect: {
extensionPath: "/war.txt?1",
},
},
},
{
id: 2,
condition: { requestDomains: ["redir"], requestMethods: ["put"] },
action: {
type: "redirect",
redirect: {
extensionPath: "/nonwar.txt?2",
},
},
},
],
});
{
browser.test.assertEq(200, req.status, "redirected to extensionPath");
browser.test.assertEq(`${location.origin}/war.txt?1`, req.url);
browser.test.assertEq("war_ext_res", await req.text());
}
// Redirects to extensionPath that is not in web_accessible_resources.
// While the initiator (extension) would be allowed to read the resource
// matching web_accessible_resources[].matches, so the load is rejected.
//
// This behavior differs from Chrome (e.g. at least in Chrome 109) that
// does allow the load to complete. Extensions who really care about
// exposing a web-accessible resource to the world can just put an all_urls
// pattern in web_accessible_resources[].matches.
await browser.test.assertRejects(
"NetworkError when attempting to fetch resource.",
"Redirect to nowar.txt, but pre-redirect host is not in web_accessible_resources[].matches"
);
browser.test.notifyPass();
}
let extension = ExtensionTestUtils.loadExtension({
background,
temporarilyInstalled: true, // Needed for granted_host_permissions
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
granted_host_permissions: true,
host_permissions: ["*://redir/*", "*://dummy/*"],
permissions: ["declarativeNetRequest"],
web_accessible_resources: [
// *://redir/* is in matches, because that is the pre-redirect host.
// *://dummy/* is in matches, because that is an initiator below.
{ resources: ["war.txt"], matches: ["*://redir/*", "*://dummy/*"] },
// without "matches", this is almost equivalent to not being listed in
// web_accessible_resources at all. This entry is listed here to verify
// that the presence of extension_ids does not somehow allow a request
// with an extension initiator to complete.
{ resources: ["nonwar.txt"], extension_ids: ["*"] },
],
},
files: {
"war.txt": "war_ext_res",
"nonwar.txt": "non_war_ext_res",
},
});
await extension.startup();
await extension.awaitFinish();
// Request from origin in host_permissions, for web-accessible resource.
Assert.deepEqual(
await contentFetch(
{ method: "post" }
),
{ status: 200, url: `${extPrefix}/war.txt?1`, body: "war_ext_res" },
"Should have got redirect to web_accessible_resources (war.txt)"
);
// Request from origin in host_permissions, for non-web-accessible resource.
let { messages } = await promiseConsoleOutput(async () => {
Assert.deepEqual(
await contentFetch(
{ method: "put" }
),
{ error: "NetworkError when attempting to fetch resource." },
"Redirect to nowar.txt, without matching web_accessible_resources[].matches"
);
});
const EXPECTED_SECURITY_ERROR = `Content at http://redir/never_reached may not load or link to ${extPrefix}/nonwar.txt?2.`;
Assert.equal(
messages.filter(m => m.message.includes(EXPECTED_SECURITY_ERROR)).length,
1,
`Should log SecurityError: ${EXPECTED_SECURITY_ERROR}`
);
// Request from origin not in host_permissions. DNR rule should not apply.
Assert.deepEqual(
await contentFetch(
{ method: "post" }
),
"Extension should not have redirected, due to lack of host permissions"
);
await extension.unload();
});