Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
"use strict";
ChromeUtils.defineESModuleGetters(this, {
ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs",
ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs",
});
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
Services.scriptloader.loadSubScript(
Services.io.newFileURI(do_get_file("head_dnr.js")).spec,
this
);
Services.scriptloader.loadSubScript(
Services.io.newFileURI(do_get_file("head_dnr_static_rules.js")).spec,
this
);
const server = createHttpServer({ hosts: ["example.com"] });
server.registerPathHandler("/", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.write("response from server");
});
add_setup(async () => {
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
Services.prefs.setBoolPref("extensions.dnr.enabled", true);
Services.prefs.setBoolPref("extensions.dnr.feedback", true);
setupTelemetryForTests();
await ExtensionTestUtils.startAddonManager();
});
add_task(async function test_load_static_rules() {
const ruleInRuleset1 = getDNRRule({
action: { type: "allow" },
condition: { resourceTypes: ["main_frame"] },
});
const ruleset1Data = [ruleInRuleset1];
const ruleset2Data = [
getDNRRule({
action: { type: "block" },
condition: { resourceTypes: ["main_frame", "script"] },
}),
];
const rule_resources = [
{
id: "ruleset_1",
enabled: true,
path: "ruleset_1.json",
},
{
id: "ruleset_2",
enabled: true,
path: "ruleset_2.json",
},
{
id: "ruleset_3",
enabled: false,
path: "ruleset_3.json",
},
];
const files = {
// Missing ruleset_3.json on purpose.
"ruleset_1.json": JSON.stringify([
{
...ruleInRuleset1,
// Unrecognized props should be allowed. We add the here instead of
// above because unrecognized properties are discarded after
// normalization, and the assertions do not expect any unrecognized
// props in the resf of the test.
condition: {
...ruleInRuleset1.condition,
nested_unexpected_prop: true,
},
unexpected_prop: true,
},
]),
"ruleset_2.json": JSON.stringify(ruleset2Data),
};
const extension = ExtensionTestUtils.loadExtension(
getDNRExtension({ rule_resources, files })
);
await extension.startup();
const extUUID = extension.uuid;
await extension.awaitMessage("bgpage:ready");
const dnrStore = ExtensionDNRStore._getStoreForTesting();
info("Verify DNRStore data for the test extension");
await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]);
await assertDNRStoreData(dnrStore, extension, {
ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
});
info("Verify matched rules using testMatchOutcome");
const testRequestMainFrame = {
type: "main_frame",
};
const testRequestScript = {
type: "script",
};
await assertDNRTestMatchOutcome(
{
extension,
testRequest: testRequestMainFrame,
expected: {
matchedRules: [{ ruleId: 1, rulesetId: "ruleset_1" }],
},
},
"Expect ruleset_1 to be matched on the main-frame test request"
);
await assertDNRTestMatchOutcome(
{
extension,
testRequest: testRequestScript,
expected: {
matchedRules: [{ ruleId: 1, rulesetId: "ruleset_2" }],
},
},
"Expect ruleset_2 to be matched on the script test request"
);
info("Verify DNRStore data persisted on disk for the test extension");
// The data will not be stored on disk until something is being changed
// from what was already available in the manifest and so in this
// test we save manually (a test for the updateEnabledRulesets will
// take care of asserting that the data has been stored automatically
// on disk when it is meant to).
await dnrStore.save(extension.extension);
const { storeFile } = dnrStore.getFilePaths(extUUID);
ok(await IOUtils.exists(storeFile), `DNR storeFile ${storeFile} found`);
// force deleting the data stored in memory to confirm if it being loaded again from
// the files stored on disk.
dnrStore._data.delete(extUUID);
dnrStore._dataPromises.delete(extUUID);
info("Verify the expected DNRStore data persisted on disk is loaded back");
const { AddonManager } = ChromeUtils.importESModule(
"resource://gre/modules/AddonManager.sys.mjs"
);
const addon = await AddonManager.getAddonByID(extension.id);
await addon.disable();
ok(
!dnrStore._dataPromises.has(extUUID),
"DNR store read data promise cleared after the extension has been disabled"
);
ok(
!dnrStore._data.has(extUUID),
"DNR store data cleared from memory after the extension has been disabled"
);
await addon.enable();
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]);
await assertDNRStoreData(dnrStore, extension, {
ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
});
info("Verify matched rules using testMatchOutcome");
await assertDNRTestMatchOutcome(
{
extension,
testRequest: testRequestMainFrame,
expected: {
matchedRules: [{ ruleId: 1, rulesetId: "ruleset_1" }],
},
},
"Expect ruleset_1 to be matched on the main-frame test request"
);
info("Verify enabled static rules updated on addon updates");
await extension.upgrade(
getDNRExtension({
version: "2.0",
rule_resources: [
{
id: "ruleset_1",
enabled: false,
path: "ruleset_1.json",
},
{
id: "ruleset_2",
enabled: true,
path: "ruleset_2.json",
},
],
files: {
"ruleset_2.json": JSON.stringify(ruleset2Data),
},
})
);
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]);
await assertDNRStoreData(dnrStore, extension, {
ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
});
info("Verify matched rules using testMatchOutcome");
await assertDNRTestMatchOutcome(
{
extension,
testRequest: testRequestMainFrame,
expected: {
matchedRules: [{ ruleId: 1, rulesetId: "ruleset_2" }],
},
},
"Expect ruleset_2 to be matched on the main-frame test request"
);
info(
"Verify enabled static rules updated on addon updates even if version in the manifest did not change"
);
await extension.upgrade(
getDNRExtension({
rule_resources: [
{
id: "ruleset_1",
enabled: true,
path: "ruleset_1.json",
},
{
id: "ruleset_2",
enabled: false,
path: "ruleset_2.json",
},
],
files: {
"ruleset_1.json": JSON.stringify(ruleset1Data),
},
})
);
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
await assertDNRStoreData(dnrStore, extension, {
ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
});
info("Verify matched rules using testMatchOutcome");
await assertDNRTestMatchOutcome(
{
extension,
testRequest: testRequestMainFrame,
expected: {
matchedRules: [{ ruleId: 1, rulesetId: "ruleset_1" }],
},
},
"Expect ruleset_2 to be matched on the main-script test request"
);
info(
"Verify updated addon version with no static rules but declarativeNetRequest permission granted"
);
await extension.upgrade(
getDNRExtension({
version: "3.0",
rule_resources: undefined,
files: {},
})
);
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, []);
await assertDNRStoreData(dnrStore, extension, {});
info("Verify matched rules using testMatchOutcome");
await assertDNRTestMatchOutcome(
{
extension,
testRequest: testRequestScript,
expected: {
matchedRules: [],
},
},
"Expect no match on the script test request on test extension without no static rules"
);
info("Verify store file removed on addon uninstall");
await extension.unload();
ok(
!dnrStore._dataPromises.has(extUUID),
"DNR store read data promise cleared after the extension has been unloaded"
);
ok(
!dnrStore._data.has(extUUID),
"DNR store data cleared from memory after the extension has been unloaded"
);
ok(
!(await IOUtils.exists(storeFile)),
`DNR storeFile ${storeFile} removed on addon uninstalled`
);
});
// As an optimization, the hasEnabledStaticRules flag in StartupCache is used
// to avoid unnecessarily trying to read and parse DNR rules from disk when an
// extension does knowingly not have any.
//
// 1. When rule reading was skipped, necessary internal state was not correctly
// initialized, and consequently updateStaticRules() would reject.
// 2. The hasDynamicRules/hasEnabledStaticRules flags were not cleared upon
// update, and consequently the flag stayed in the initial state (false),
// and the previously stored rules did not apply after a browser restart.
//
// See also test_register_dynamic_rules_after_restart in
// test_ext_dnr_dynamic_rules.js for the similar test with dynamic rules.
add_task(async function test_enable_disabled_static_rules_after_restart() {
// Through this test, we confirm that the underlying "expensive" rule data
// storage is accessed when needed, and skipped when no relevant rules had
// been detected at the previous session. Caching too much or too little in
// StartupCache will trigger test failures in assertStoreReadsSinceLastCall.
const sandboxStoreSpies = sinon.createSandbox();
const dnrStore = ExtensionDNRStore._getStoreForTesting();
const spyReadDNRStore = sandboxStoreSpies.spy(dnrStore, "_readData");
function assertStoreReadsSinceLastCall(expectedCount, description) {
equal(spyReadDNRStore.callCount, expectedCount, description);
spyReadDNRStore.resetHistory();
}
const rule_resources = [
{
id: "ruleset_initially_disabled",
enabled: false,
path: "ruleset_initially_disabled.json",
},
];
const files = {
"ruleset_initially_disabled.json": JSON.stringify([getDNRRule()]),
};
const extension = ExtensionTestUtils.loadExtension(
getDNRExtension({ rule_resources, files, id: "dnr@initially-disabled" })
);
await extension.startup();
await extension.awaitMessage("bgpage:ready");
assertStoreReadsSinceLastCall(1, "Read once at initial startup");
await AddonTestUtils.promiseRestartManager();
// Note: ordinarily, event pages do not wake up after a browser restart,
// unless a relevant event such as runtime.onStartup is triggered. But as
// noted in bug 1822735, "event pages without any event listeners" will be
// awakened after a restart, so we can expect the bgpage:ready message here:
await extension.awaitMessage("bgpage:ready");
assertStoreReadsSinceLastCall(
0,
"Read skipped due to hasEnabledStaticRules=false"
);
// Regression test 1: before bug 1921353 was fixed, the test got stuck here
// because the updateStaticRules() call after a restart unexpectedly failed
// with "can't access property "disabledStaticRuleIds", data is undefined".
extension.sendMessage("updateStaticRules", {
rulesetId: "ruleset_initially_disabled",
enableRuleIds: [1234], // Does not exist, does not matter.
});
info("Trying to call declarativeNetRequest.updateStaticRules()...");
Assert.deepEqual(
await extension.awaitMessage("updateStaticRules:done"),
[undefined],
"updateStaticRules() succeeded without error"
);
// Now transition from zero rulesets to one (zero_to_one_rule).
extension.sendMessage("updateEnabledRulesets", {
enableRulesetIds: ["ruleset_initially_disabled"],
});
info("Trying to enable ruleset_initially_disabled...");
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, ["ruleset_initially_disabled"]);
assertStoreReadsSinceLastCall(0, "No further reads before restart");
await AddonTestUtils.promiseRestartManager();
await extension.awaitMessage("bgpage:ready");
// Regression test 2: before bug 1921353 was fixed, even with a fix to the
// previous bug, the static rules would not be read at startup due to the
// wrong cached hasEnabledStaticRules flag in StartupCache.
// Verify that the static rules are still enabled.
assertStoreReadsSinceLastCall(1, "Read due to hasEnabledStaticRules=true");
await assertDNRGetEnabledRulesets(extension, ["ruleset_initially_disabled"]);
// For full coverage, also verify that when all static rules are disabled,
// that the initialization is skipped as expected.
extension.sendMessage("updateEnabledRulesets", {
disableRulesetIds: ["ruleset_initially_disabled"],
});
info("Trying to disable ruleset_initially_disabled...");
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, []);
await AddonTestUtils.promiseRestartManager();
await extension.awaitMessage("bgpage:ready");
assertStoreReadsSinceLastCall(0, "Read skipped because rules were disabled");
await assertDNRGetEnabledRulesets(extension, []);
// declarativeNetRequest.getEnabledStaticRulesets() queries in-memory state,
// so we do not expect another read from disk.
assertStoreReadsSinceLastCall(0, "Read still skipped despite API call");
await extension.unload();
sandboxStoreSpies.restore();
});
add_task(async function test_load_from_corrupted_data() {
const ruleset1Data = [
getDNRRule({
action: { type: "allow" },
condition: { resourceTypes: ["main_frame"] },
}),
];
const rule_resources = [
{
id: "ruleset_1",
enabled: true,
path: "ruleset_1.json",
},
];
const files = {
"ruleset_1.json": JSON.stringify(ruleset1Data),
};
const extension = ExtensionTestUtils.loadExtension(
getDNRExtension({ rule_resources, files })
);
await extension.startup();
const extUUID = extension.uuid;
await extension.awaitMessage("bgpage:ready");
const dnrStore = ExtensionDNRStore._getStoreForTesting();
info("Verify DNRStore data for the test extension");
await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
await assertDNRStoreData(dnrStore, extension, {
ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
});
info("Verify DNRStore data after loading corrupted store data");
await dnrStore.save(extension.extension);
const { storeFile } = dnrStore.getFilePaths(extUUID);
ok(await IOUtils.exists(storeFile), `DNR storeFile ${storeFile} found`);
const nonCorruptedData = await IOUtils.readJSON(storeFile, {
decompress: true,
});
async function testLoadedRulesAfterDataCorruption({
name,
asyncWriteStoreFile,
expectedCorruptFile,
}) {
info(`Tempering DNR store data: ${name}`);
await extension.addon.disable();
ok(
!dnrStore._dataPromises.has(extUUID),
"DNR store read data promise cleared after the extension has been disabled"
);
ok(
!dnrStore._data.has(extUUID),
"DNR store data cleared from memory after the extension has been disabled"
);
// Make sure we remove a previous corrupt file in case there is one from a previous run.
await IOUtils.remove(expectedCorruptFile, { ignoreAbsent: true });
await asyncWriteStoreFile();
await extension.addon.enable();
await extension.awaitMessage("bgpage:ready");
info("Verify DNRStore data for the test extension");
await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
await assertDNRStoreData(dnrStore, extension, {
ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
});
await TestUtils.waitForCondition(
() => IOUtils.exists(`${expectedCorruptFile}`),
`Wait for the "${expectedCorruptFile}" file to have been created`
);
}
await testLoadedRulesAfterDataCorruption({
name: "invalid lz4 header",
asyncWriteStoreFile: () =>
IOUtils.writeUTF8(storeFile, "not an lz4 compressed file", {
compress: false,
}),
expectedCorruptFile: `${storeFile}.corrupt`,
});
await testLoadedRulesAfterDataCorruption({
name: "invalid json data",
asyncWriteStoreFile: () =>
IOUtils.writeUTF8(storeFile, "invalid json data", { compress: true }),
expectedCorruptFile: `${storeFile}-1.corrupt`,
});
await testLoadedRulesAfterDataCorruption({
name: "empty json data",
asyncWriteStoreFile: () =>
IOUtils.writeUTF8(storeFile, "{}", { compress: true }),
expectedCorruptFile: `${storeFile}-2.corrupt`,
});
await testLoadedRulesAfterDataCorruption({
name: "invalid staticRulesets property type",
asyncWriteStoreFile: () =>
IOUtils.writeUTF8(
storeFile,
JSON.stringify({
schemaVersion: nonCorruptedData.schemaVersion,
extVersion: extension.extension.version,
staticRulesets: "Not an array",
}),
{ compress: true }
),
expectedCorruptFile: `${storeFile}-3.corrupt`,
});
await extension.unload();
});
add_task(async function test_ruleset_validation() {
const invalidRulesetIdCases = [
{
description: "empty ruleset id",
rule_resources: [
{
// Invalid empty ruleset id.
id: "",
path: "ruleset_0.json",
enabled: true,
},
],
expected: [
// Validation error emitted from the manifest schema validation.
{
message: /rule_resources\.0\.id: String "" must match/,
},
],
},
{
description: "invalid ruleset id starting with '_'",
rule_resources: [
{
// Invalid empty ruleset id.
id: "_invalid_ruleset_id",
path: "ruleset_0.json",
enabled: true,
},
],
expected: [
// Validation error emitted from the manifest schema validation.
{
message:
/rule_resources\.0\.id: String "_invalid_ruleset_id" must match/,
},
],
},
{
description: "duplicated ruleset ids",
rule_resources: [
{
id: "ruleset_2",
path: "ruleset_2.json",
enabled: true,
},
{
// Duplicated ruleset id.
id: "ruleset_2",
path: "duplicated_ruleset_2.json",
enabled: true,
},
{
id: "ruleset_3",
path: "ruleset_3.json",
enabled: true,
},
{
// Other duplicated ruleset id.
id: "ruleset_3",
path: "duplicated_ruleset_3.json",
enabled: true,
},
],
// NOTE: this is currently a warning logged from onManifestEntry, and so it would actually
// fail in test harness due to the manifest warning, because it is too late at that point
// the addon is technically already starting at that point.
expectInstallFailed: false,
expected: [
{
message:
/declarative_net_request: Static ruleset ids should be unique.*: "ruleset_2" at index 1, "ruleset_3" at index 3/,
},
],
},
{
description: "missing mandatory path",
rule_resources: [
{
// Missing mandatory path.
id: "ruleset_3",
enabled: true,
},
],
expected: [
{
message: /rule_resources\.0: Property "path" is required/,
},
],
},
{
description: "missing mandatory id",
rule_resources: [
{
// Missing mandatory id.
enabled: true,
path: "missing_ruleset_id.json",
},
],
expected: [
{
message: /rule_resources\.0: Property "id" is required/,
},
],
},
{
description: "duplicated ruleset path",
rule_resources: [
{
id: "ruleset_2",
path: "ruleset_2.json",
enabled: true,
},
{
// Duplicate path.
id: "ruleset_3",
path: "ruleset_2.json",
enabled: true,
},
],
// NOTE: we couldn't get on agreement about making this a manifest validation error, apparently Chrome doesn't validate it and doesn't
// even report any warning, and so it is logged only as an informative warning but without triggering an install failure.
expectInstallFailed: false,
expected: [
{
message:
/declarative_net_request: Static rulesets paths are not unique.*: ".*ruleset_2.json" at index 1/,
},
],
},
{
description: "missing mandatory enabled",
rule_resources: [
{
id: "ruleset_without_enabled",
path: "ruleset.json",
},
],
expected: [
{
message: /rule_resources\.0: Property "enabled" is required/,
},
],
},
{
description: "allows and warns additional properties",
declarative_net_request: {
unexpected_prop: true,
rule_resources: [
{
id: "ruleset1",
path: "ruleset1.json",
enabled: false,
unexpected_prop: true,
},
],
},
expectInstallFailed: false,
expected: [
{
message:
/declarative_net_request.unexpected_prop: An unexpected property was found/,
},
{
message:
/rule_resources.0.unexpected_prop: An unexpected property was found/,
},
],
},
{
description: "invalid ruleset JSON - unexpected comments",
rule_resources: [
{
id: "invalid_ruleset_with_comments",
path: "invalid_ruleset_with_comments.json",
enabled: true,
},
],
files: {
"invalid_ruleset_with_comments.json":
"/* an unexpected inline comment */\n[]",
},
expectInstallFailed: false,
expected: [
{
message:
/Reading declarative_net_request .*invalid_ruleset_with_comments\.json: JSON.parse: unexpected character/,
},
],
},
{
description: "invalid ruleset JSON - empty string",
rule_resources: [
{
id: "invalid_ruleset_emptystring",
path: "invalid_ruleset_emptystring.json",
enabled: true,
},
],
files: {
"invalid_ruleset_emptystring.json": JSON.stringify(""),
},
expectInstallFailed: false,
expected: [
{
message:
/Reading declarative_net_request .*invalid_ruleset_emptystring\.json: rules file must contain an Array/,
},
],
},
{
description: "invalid ruleset JSON - object",
rule_resources: [
{
id: "invalid_ruleset_object",
path: "invalid_ruleset_object.json",
enabled: true,
},
],
files: {
"invalid_ruleset_object.json": JSON.stringify({}),
},
expectInstallFailed: false,
expected: [
{
message:
/Reading declarative_net_request .*invalid_ruleset_object\.json: rules file must contain an Array/,
},
],
},
{
description: "invalid ruleset JSON - null",
rule_resources: [
{
id: "invalid_ruleset_null",
path: "invalid_ruleset_null.json",
enabled: true,
},
],
files: {
"invalid_ruleset_null.json": JSON.stringify(null),
},
expectInstallFailed: false,
expected: [
{
message:
/Reading declarative_net_request .*invalid_ruleset_null\.json: rules file must contain an Array/,
},
],
},
];
for (const {
description,
declarative_net_request,
rule_resources,
files,
expected,
expectInstallFailed = true,
} of invalidRulesetIdCases) {
info(`Test manifest validation: ${description}`);
let extension = ExtensionTestUtils.loadExtension(
getDNRExtension({ rule_resources, declarative_net_request, files })
);
const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
ExtensionTestUtils.failOnSchemaWarnings(false);
if (expectInstallFailed) {
await Assert.rejects(
extension.startup(),
/Install failed/,
"Expected install to fail"
);
} else {
await extension.startup();
await extension.awaitMessage("bgpage:ready");
await extension.unload();
}
ExtensionTestUtils.failOnSchemaWarnings(true);
});
AddonTestUtils.checkMessages(messages, { expected });
}
});
add_task(async function test_updateEnabledRuleset_id_validation() {
const rule_resources = [
{
id: "ruleset_1",
enabled: true,
path: "ruleset_1.json",
},
{
id: "ruleset_2",
enabled: false,
path: "ruleset_2.json",
},
];
const ruleset1Data = [
getDNRRule({
action: { type: "allow" },
condition: { resourceTypes: ["main_frame"] },
}),
];
const ruleset2Data = [
getDNRRule({
action: { type: "block" },
condition: { resourceTypes: ["main_frame", "script"] },
}),
];
const files = {
"ruleset_1.json": JSON.stringify(ruleset1Data),
"ruleset_2.json": JSON.stringify(ruleset2Data),
};
let extension = ExtensionTestUtils.loadExtension(
getDNRExtension({ rule_resources, files })
);
await extension.startup();
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
const dnrStore = ExtensionDNRStore._getStoreForTesting();
await assertDNRStoreData(dnrStore, extension, {
ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
});
const invalidStaticRulesetIds = [
// The following two are reserved for session and dynamic rules.
"_session",
"_dynamic",
"ruleset_non_existing",
];
for (const invalidRSId of invalidStaticRulesetIds) {
extension.sendMessage(
"updateEnabledRulesets",
// Only in rulesets to be disabled.
{ disableRulesetIds: [invalidRSId] },
// Only in rulesets to be enabled.
{ enableRulesetIds: [invalidRSId] },
// In both rulesets to be enabled and disabled.
{ disableRulesetIds: [invalidRSId], enableRulesetIds: [invalidRSId] },
// Along with existing rulesets (and expected the existing rulesets
// to stay unchanged due to the invalid ruleset ids.)
{
disableRulesetIds: [invalidRSId, "ruleset_1"],
enableRulesetIds: [invalidRSId, "ruleset_2"],
}
);
const [
resInDisable,
resInEnable,
resInEnableAndDisable,
resInSameRequestAsValid,
] = await extension.awaitMessage("updateEnabledRulesets:done");
await Assert.rejects(
Promise.reject(resInDisable?.rejectedWithErrorMessage),
new RegExp(`Invalid ruleset id: "${invalidRSId}"`),
`Got the expected rejection on invalid ruleset id "${invalidRSId}" in disableRulesetIds`
);
await Assert.rejects(
Promise.reject(resInEnable?.rejectedWithErrorMessage),
new RegExp(`Invalid ruleset id: "${invalidRSId}"`),
`Got the expected rejection on invalid ruleset id "${invalidRSId}" in enableRulesetIds`
);
await Assert.rejects(
Promise.reject(resInEnableAndDisable?.rejectedWithErrorMessage),
new RegExp(`Invalid ruleset id: "${invalidRSId}"`),
`Got the expected rejection on invalid ruleset id "${invalidRSId}" in both enable/disableRulesetIds`
);
await Assert.rejects(
Promise.reject(resInSameRequestAsValid?.rejectedWithErrorMessage),
new RegExp(`Invalid ruleset id: "${invalidRSId}"`),
`Got the expected rejection on invalid ruleset id "${invalidRSId}" along with valid ruleset ids`
);
}
// Confirm that the expected rulesets didn't change neither.
await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
await assertDNRStoreData(dnrStore, extension, {
ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data),
});
// - List the same ruleset ids more than ones is expected to work and
// to be resulting in the same set of rules being enabled
// - Disabling and Enabling the same ruleset id should result in the
// ruleset being enabled.
await extension.sendMessage("updateEnabledRulesets", {
disableRulesetIds: [
"ruleset_1",
"ruleset_1",
"ruleset_2",
"ruleset_2",
"ruleset_2",
],
enableRulesetIds: ["ruleset_2", "ruleset_2"],
});
Assert.deepEqual(
await extension.awaitMessage("updateEnabledRulesets:done"),
[undefined],
"Expect the updateEnabledRulesets to result successfully"
);
await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]);
await assertDNRStoreData(dnrStore, extension, {
ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data),
});
await extension.unload();
});
add_task(async function test_tabId_conditions_invalid_in_static_rules() {
const ruleset1_with_tabId_condition = [
getDNRRule({ id: 1, condition: { tabIds: [1] } }),
getDNRRule({ id: 3, condition: { urlFilter: "valid-ruleset1-rule" } }),
];
const ruleset2_with_excludeTabId_condition = [
getDNRRule({ id: 2, condition: { excludedTabIds: [1] } }),
getDNRRule({ id: 3, condition: { urlFilter: "valid-ruleset2-rule" } }),
];
const rule_resources = [
{
id: "ruleset1_with_tabId_condition",
enabled: true,
path: "ruleset1.json",
},
{
id: "ruleset2_with_excludeTabId_condition",
enabled: true,
path: "ruleset2.json",
},
];
const files = {
"ruleset1.json": JSON.stringify(ruleset1_with_tabId_condition),
"ruleset2.json": JSON.stringify(ruleset2_with_excludeTabId_condition),
};
const extension = ExtensionTestUtils.loadExtension(
getDNRExtension({
id: "tabId-invalid-in-session-rules@mochitest",
rule_resources,
files,
})
);
const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
ExtensionTestUtils.failOnSchemaWarnings(false);
await extension.startup();
ExtensionTestUtils.failOnSchemaWarnings(true);
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, [
"ruleset1_with_tabId_condition",
"ruleset2_with_excludeTabId_condition",
]);
});
AddonTestUtils.checkMessages(messages, {
expected: [
{
message:
/"ruleset1_with_tabId_condition": tabIds and excludedTabIds can only be specified in session rules/,
},
{
message:
/"ruleset2_with_excludeTabId_condition": tabIds and excludedTabIds can only be specified in session rules/,
},
],
});
info("Expect the invalid rule to not be enabled");
const dnrStore = ExtensionDNRStore._getStoreForTesting();
// Expect the two valid rules to have been loaded as expected.
await assertDNRStoreData(dnrStore, extension, {
ruleset1_with_tabId_condition: getSchemaNormalizedRules(extension, [
ruleset1_with_tabId_condition[1],
]),
ruleset2_with_excludeTabId_condition: getSchemaNormalizedRules(extension, [
ruleset2_with_excludeTabId_condition[1],
]),
});
await extension.unload();
});
add_task(async function test_dnr_all_rules_disabled_allowed() {
const ruleset1 = [
getDNRRule({ id: 3, condition: { urlFilter: "valid-ruleset1-rule" } }),
];
const rule_resources = [
{
id: "ruleset1",
enabled: true,
path: "ruleset1.json",
},
];
const files = {
"ruleset1.json": JSON.stringify(ruleset1),
};
const extension = ExtensionTestUtils.loadExtension(
getDNRExtension({
id: "all-static-rulesets-disabled-allowed@mochitest",
rule_resources,
files,
})
);
await extension.startup();
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, ["ruleset1"]);
const dnrStore = ExtensionDNRStore._getStoreForTesting();
await assertDNRStoreData(dnrStore, extension, {
ruleset1: getSchemaNormalizedRules(extension, ruleset1),
});
info("Disable static ruleset1");
extension.sendMessage("updateEnabledRulesets", {
disableRulesetIds: ["ruleset1"],
});
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, []);
await assertDNRStoreData(dnrStore, extension, {});
info("Verify that static ruleset1 is still disable after browser restart");
// NOTE: promiseRestartManager will not be enough to make sure the
// DNR store data for the test extension is going to be loaded from
// the DNR startup cache file.
// See test_ext_dnr_startup_cache.js for a test case that more completely
// simulates ExtensionDNRStore initialization on browser restart.
await AddonTestUtils.promiseRestartManager();
await extension.awaitStartup;
await ExtensionDNR.ensureInitialized(extension.extension);
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, []);
await assertDNRStoreData(dnrStore, extension, {});
await extension.unload();
});
add_task(async function test_static_rules_telemetry() {
resetTelemetryData();
const ruleset1 = [
getDNRRule({
id: 1,
action: { type: "block" },
condition: {
resourceTypes: ["xmlhttprequest"],
requestDomains: ["example.com"],
},
}),
];
const ruleset2 = [
getDNRRule({
id: 1,
action: { type: "block" },
condition: {
resourceTypes: ["xmlhttprequest"],
requestDomains: ["example.org"],
},
}),
getDNRRule({
id: 2,
action: { type: "block" },
condition: {
resourceTypes: ["xmlhttprequest"],
requestDomains: ["example2.org"],
},
}),
];
const rule_resources = [
{
id: "ruleset1",
enabled: false,
path: "ruleset1.json",
},
{
id: "ruleset2",
enabled: false,
path: "ruleset2.json",
},
];
const files = {
"ruleset1.json": JSON.stringify(ruleset1),
"ruleset2.json": JSON.stringify(ruleset2),
};
const extension = ExtensionTestUtils.loadExtension(
getDNRExtension({
id: "tabId-invalid-in-session-rules@mochitest",
rule_resources,
files,
})
);
assertDNRTelemetryMetricsNoSamples(
[
{
metric: "validateRulesTime",
mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
mirroredType: "histogram",
},
{
metric: "evaluateRulesTime",
mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS",
mirroredType: "histogram",
},
],
"before test extension have been loaded"
);
await extension.startup();
await extension.awaitMessage("bgpage:ready");
await assertDNRGetEnabledRulesets(extension, []);
assertDNRTelemetryMetricsNoSamples(
[
{
metric: "validateRulesTime",
mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
mirroredType: "histogram",
},
],
"after test extension loaded with all static rulesets disabled"
);
info("Enable static ruleset1");
extension.sendMessage("updateEnabledRulesets", {
enableRulesetIds: ["ruleset1"],
});
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, ["ruleset1"]);
// Expect one sample after enabling ruleset1.
let expectedValidateRulesTimeSamples = 1;
assertDNRTelemetryMetricsSamplesCount(
[
{
metric: "validateRulesTime",
mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
mirroredType: "histogram",
expectedSamplesCount: expectedValidateRulesTimeSamples,
},
],
"after enabling static rulesets1"
);
info("Enable static ruleset2");
extension.sendMessage("updateEnabledRulesets", {
enableRulesetIds: ["ruleset2"],
});
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, ["ruleset1", "ruleset2"]);
// Expect one new sample after enabling ruleset2.
expectedValidateRulesTimeSamples += 1;
assertDNRTelemetryMetricsSamplesCount(
[
{
metric: "validateRulesTime",
mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
mirroredType: "histogram",
expectedSamplesCount: expectedValidateRulesTimeSamples,
},
],
"after enabling static rulesets2"
);
await extension.addon.disable();
assertDNRTelemetryMetricsSamplesCount(
[
{
metric: "validateRulesTime",
mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
mirroredType: "histogram",
expectedSamplesCount: expectedValidateRulesTimeSamples,
},
],
"no new samples expected after disabling test extension"
);
await extension.addon.enable();
await extension.awaitMessage("bgpage:ready");
await ExtensionDNR.ensureInitialized(extension.extension);
// Expect 2 new samples after re-enabling the addon with
// the 2 rulesets enabled being loaded from the DNR store file.
expectedValidateRulesTimeSamples += 2;
assertDNRTelemetryMetricsSamplesCount(
[
{
metric: "validateRulesTime",
mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
mirroredType: "histogram",
expectedSamplesCount: expectedValidateRulesTimeSamples,
},
],
"after re-enabling test extension"
);
info("Disable static ruleset1");
extension.sendMessage("updateEnabledRulesets", {
disableRulesetIds: ["ruleset1"],
});
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, ["ruleset2"]);
assertDNRTelemetryMetricsSamplesCount(
[
{
metric: "validateRulesTime",
mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
mirroredType: "histogram",
expectedSamplesCount: expectedValidateRulesTimeSamples,
},
],
"no new validation should be hit after disabling ruleset1"
);
info("Verify telemetry recorded on rules evaluation");
extension.sendMessage("updateEnabledRulesets", {
enableRulesetIds: ["ruleset1"],
disableRulesetIds: ["ruleset2"],
});
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, ["ruleset1"]);
assertDNRTelemetryMetricsNoSamples(
[
{
metric: "evaluateRulesTime",
mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS",
mirroredType: "histogram",
},
{
metric: "evaluateRulesCountMax",
mirroredName: "extensions.apis.dnr.evaluate_rules_count_max",
mirroredType: "scalar",
},
],
"before any request have been intercepted"
);
Assert.equal(
await fetch("http://example.com/").then(res => res.text()),
"response from server",
"DNR should not block system requests"
);
assertDNRTelemetryMetricsNoSamples(
[
{
metric: "evaluateRulesTime",
mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS",
mirroredType: "histogram",
},
{
metric: "evaluateRulesCountMax",
mirroredName: "extensions.apis.dnr.evaluate_rules_count_max",
mirroredType: "scalar",
},
],
"after restricted request have been intercepted (but no rules evaluated)"
);
const page = await ExtensionTestUtils.loadContentPage("http://example.com");
const callPageFetch = async () => {
Assert.equal(
await page.spawn([], () => {
return this.content.fetch("http://example.com/").then(
res => res.text(),
err => err.message
);
}),
"NetworkError when attempting to fetch resource.",
"DNR should have blocked test request to example.com"
);
};
// Expect one sample recorded on evaluating rules for the
// top level navigation.
let expectedEvaluateRulesTimeSamples = 1;
assertDNRTelemetryMetricsSamplesCount(
[
{
metric: "evaluateRulesTime",
mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS",
mirroredType: "histogram",
expectedSamplesCount: expectedEvaluateRulesTimeSamples,
},
],
"evaluateRulesTime should be collected after evaluated rulesets"
);
// Expect same number of rules included in the single ruleset
// currently enabled.
let expectedEvaluateRulesCountMax = ruleset1.length;
assertDNRTelemetryMetricsGetValueEq(
[
{
metric: "evaluateRulesCountMax",
mirroredName: "extensions.apis.dnr.evaluate_rules_count_max",
mirroredType: "scalar",
expectedGetValue: expectedEvaluateRulesCountMax,
},
],
"evaluateRulesCountMax should be collected after evaluated rulesets1"
);
await callPageFetch();
// Expect one new sample reported on evaluating rules for the
// first fetch request originated from the test page.
expectedEvaluateRulesTimeSamples += 1;
assertDNRTelemetryMetricsSamplesCount(
[
{
metric: "evaluateRulesTime",
mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS",
mirroredType: "histogram",
expectedSamplesCount: expectedEvaluateRulesTimeSamples,
},
],
"evaluateRulesTime should be collected after evaluated rulesets"
);
extension.sendMessage("updateEnabledRulesets", {
enableRulesetIds: ["ruleset2"],
});
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, ["ruleset1", "ruleset2"]);
await callPageFetch();
// Expect 3 rules with both rulesets enabled
// (1 from ruleset1 and 2 more from ruleset2).
expectedEvaluateRulesCountMax += ruleset2.length;
assertDNRTelemetryMetricsGetValueEq(
[
{
metric: "evaluateRulesCountMax",
mirroredName: "extensions.apis.dnr.evaluate_rules_count_max",
mirroredType: "scalar",
expectedGetValue: expectedEvaluateRulesCountMax,
},
],
"evaluateRulesCountMax should have been increased after enabling ruleset2"
);
extension.sendMessage("updateEnabledRulesets", {
disableRulesetIds: ["ruleset2"],
});
await extension.awaitMessage("updateEnabledRulesets:done");
await assertDNRGetEnabledRulesets(extension, ["ruleset1"]);
await callPageFetch();
assertDNRTelemetryMetricsGetValueEq(
[
{
metric: "evaluateRulesCountMax",
mirroredName: "extensions.apis.dnr.evaluate_rules_count_max",
mirroredType: "scalar",
expectedGetValue: expectedEvaluateRulesCountMax,
},
],
"evaluateRulesCountMax should have not been decreased after disabling ruleset2"
);
await page.close();
await extension.unload();
});