summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js1849
1 files changed, 1849 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js
new file mode 100644
index 0000000000..19c869b149
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js
@@ -0,0 +1,1849 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs",
+ ExtensionDNRLimits: "resource://gre/modules/ExtensionDNRLimits.sys.mjs",
+ ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+Services.scriptloader.loadSubScript(
+ Services.io.newFileURI(do_get_file("head_dnr.js")).spec,
+ this
+);
+
+const server = createHttpServer({ hosts: ["example.com"] });
+server.registerPathHandler("/", (req, res) => {
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.write("response from server");
+});
+
+function backgroundWithDNRAPICallHandlers() {
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ let result;
+ switch (msg) {
+ case "getEnabledRulesets":
+ result = await browser.declarativeNetRequest.getEnabledRulesets();
+ break;
+ case "getAvailableStaticRuleCount":
+ result =
+ await browser.declarativeNetRequest.getAvailableStaticRuleCount();
+ break;
+ case "testMatchOutcome":
+ result = await browser.declarativeNetRequest
+ .testMatchOutcome(...args)
+ .catch(err =>
+ browser.test.fail(
+ `Unexpected rejection from testMatchOutcome call: ${err}`
+ )
+ );
+ break;
+ case "updateEnabledRulesets":
+ // Run (one or more than one concurrently) updateEnabledRulesets calls
+ // and report back the results.
+ result = await Promise.all(
+ args.map(arg => {
+ return browser.declarativeNetRequest
+ .updateEnabledRulesets(arg)
+ .catch(err => {
+ return { rejectedWithErrorMessage: err.message };
+ });
+ })
+ );
+ break;
+ default:
+ browser.test.fail(`Unexpected test message: ${msg}`);
+ return;
+ }
+
+ browser.test.sendMessage(`${msg}:done`, result);
+ });
+
+ browser.test.sendMessage("bgpage:ready");
+}
+
+function getDNRExtension({
+ id = "test-dnr-static-rules@test-extension",
+ version = "1.0",
+ background = backgroundWithDNRAPICallHandlers,
+ useAddonManager = "permanent",
+ rule_resources,
+ declarative_net_request,
+ files,
+}) {
+ // Omit declarative_net_request if rule_resources isn't defined
+ // (because declarative_net_request fails the manifest validation
+ // if rule_resources is missing).
+ const dnr = rule_resources ? { rule_resources } : undefined;
+
+ return {
+ background,
+ useAddonManager,
+ manifest: {
+ manifest_version: 3,
+ version,
+ permissions: ["declarativeNetRequest", "declarativeNetRequestFeedback"],
+ // Needed to make sure the upgraded extension will have the same id and
+ // same uuid (which is mapped based on the extension id).
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ declarative_net_request: declarative_net_request
+ ? { ...declarative_net_request, ...(dnr ?? {}) }
+ : dnr,
+ },
+ files,
+ };
+}
+
+const assertDNRTestMatchOutcome = async (
+ { extension, testRequest, expected },
+ assertMessage
+) => {
+ extension.sendMessage("testMatchOutcome", testRequest);
+ Assert.deepEqual(
+ expected,
+ await extension.awaitMessage("testMatchOutcome:done"),
+ assertMessage ??
+ "Got the expected matched rules from testMatchOutcome API call"
+ );
+};
+
+const assertDNRGetAvailableStaticRuleCount = async (
+ extensionTestWrapper,
+ expectedCount,
+ assertMessage
+) => {
+ extensionTestWrapper.sendMessage("getAvailableStaticRuleCount");
+ Assert.deepEqual(
+ await extensionTestWrapper.awaitMessage("getAvailableStaticRuleCount:done"),
+ expectedCount,
+ assertMessage ??
+ "Got the expected count value from dnr.getAvailableStaticRuleCount API method"
+ );
+};
+
+const assertDNRGetEnabledRulesets = async (
+ extensionTestWrapper,
+ expectedRulesetIds
+) => {
+ extensionTestWrapper.sendMessage("getEnabledRulesets");
+ Assert.deepEqual(
+ await extensionTestWrapper.awaitMessage("getEnabledRulesets:done"),
+ expectedRulesetIds,
+ "Got the expected enabled ruleset ids from dnr.getEnabledRulesets API method"
+ );
+};
+
+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 ruleset1Data = [
+ getDNRRule({
+ action: { type: "allow" },
+ condition: { resourceTypes: ["main_frame"] },
+ }),
+ ];
+ 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(ruleset1Data),
+ "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 = {
+ url: "https://example.com/some-dummy-url",
+ type: "main_frame",
+ };
+ const testRequestScript = {
+ url: "https://example.com/some-dummy-url.js",
+ 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`
+ );
+});
+
+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_getAvailableStaticRulesCountAndLimits() {
+ // NOTE: this test is going to load and validate the maximum amount of static rules
+ // that an extension can enable, which on slower builds (in particular in tsan builds,
+ // e.g. see Bug 1803801) have a higher chance that the test extension may have hit the
+ // idle timeout and being suspended by the time the test is going to trigger API method
+ // calls through test API events (which do not expect the lifetime of the event page).
+ Services.prefs.setBoolPref("extensions.background.idle.enabled", false);
+
+ const dnrStore = ExtensionDNRStore._getStoreForTesting();
+ const { GUARANTEED_MINIMUM_STATIC_RULES } = ExtensionDNRLimits;
+ equal(
+ typeof GUARANTEED_MINIMUM_STATIC_RULES,
+ "number",
+ "Expect GUARANTEED_MINIMUM_STATIC_RULES to be a number"
+ );
+
+ const availableStaticRulesCount = GUARANTEED_MINIMUM_STATIC_RULES;
+
+ const rule_resources = [
+ {
+ id: "ruleset_0",
+ path: "/ruleset_0.json",
+ enabled: true,
+ },
+ {
+ id: "ruleset_1",
+ path: "/ruleset_1.json",
+ enabled: true,
+ },
+ // A ruleset initially disabled (to make sure it doesn't count for the
+ // rules count limit).
+ {
+ id: "ruleset_disabled",
+ path: "/ruleset_disabled.json",
+ enabled: false,
+ },
+ // A ruleset including an invalid rule and valid rule.
+ {
+ id: "ruleset_withInvalid",
+ path: "/ruleset_withInvalid.json",
+ enabled: false,
+ },
+ // An empty ruleset (to make sure it can still be enabled/disabled just fine,
+ // e.g. in case on some browser version all rules are technically invalid).
+ {
+ id: "ruleset_empty",
+ path: "/ruleset_empty.json",
+ enabled: false,
+ },
+ ];
+
+ const files = {};
+ const rules = {};
+
+ const rulesetDisabledData = [getDNRRule({ id: 1 })];
+ const ruleValid = getDNRRule({ id: 2, action: { type: "allow" } });
+ const rulesetWithInvalidData = [
+ getDNRRule({ id: 1, action: { type: "invalid_action" } }),
+ ruleValid,
+ ];
+
+ rules.ruleset_0 = [getDNRRule({ id: 1 }), getDNRRule({ id: 2 })];
+
+ rules.ruleset_1 = [];
+ for (let i = 0; i < availableStaticRulesCount; i++) {
+ rules.ruleset_1.push(getDNRRule({ id: i + 1 }));
+ }
+
+ for (const [k, v] of Object.entries(rules)) {
+ files[`${k}.json`] = JSON.stringify(v);
+ }
+ files[`ruleset_disabled.json`] = JSON.stringify(rulesetDisabledData);
+ files[`ruleset_withInvalid.json`] = JSON.stringify(rulesetWithInvalidData);
+ files[`ruleset_empty.json`] = JSON.stringify([]);
+
+ const extension = ExtensionTestUtils.loadExtension(
+ getDNRExtension({
+ id: "dnr-getAvailable-count-@mochitest",
+ rule_resources,
+ files,
+ })
+ );
+
+ await extension.startup();
+ await extension.awaitMessage("bgpage:ready");
+
+ async function updateEnabledRulesets({ expectedErrorMessage, ...options }) {
+ // Note: options = { disableRulesetIds, enableRulesetIds }
+ extension.sendMessage("updateEnabledRulesets", options);
+ let [result] = await extension.awaitMessage("updateEnabledRulesets:done");
+ if (expectedErrorMessage) {
+ Assert.deepEqual(
+ result,
+ { rejectedWithErrorMessage: expectedErrorMessage },
+ "updateEnabledRulesets() should reject with the given error"
+ );
+ } else {
+ Assert.deepEqual(
+ result,
+ undefined,
+ "updateEnabledRulesets() should resolve without error"
+ );
+ }
+ }
+
+ const expectedEnabledRulesets = {};
+ expectedEnabledRulesets.ruleset_0 = getSchemaNormalizedRules(
+ extension,
+ rules.ruleset_0
+ );
+
+ info(
+ "Expect ruleset_1 to not be enabled because along with ruleset_0 exceeded the static rules count limit"
+ );
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
+
+ await assertDNRGetAvailableStaticRuleCount(
+ extension,
+ availableStaticRulesCount - rules.ruleset_0.length,
+ "Got the available static rule count on ruleset_0 initially enabled"
+ );
+
+ // Try to enable ruleset_1 again from the API method.
+ await updateEnabledRulesets({
+ enableRulesetIds: ["ruleset_1"],
+ expectedErrorMessage: `Number of rules across all enabled static rulesets exceeds GUARANTEED_MINIMUM_STATIC_RULES if ruleset "ruleset_1" were to be enabled.`,
+ });
+
+ info(
+ "Expect ruleset_1 to not be enabled because still exceeded the static rules count limit"
+ );
+ await assertDNRGetEnabledRulesets(extension, ["ruleset_0"]);
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
+
+ await assertDNRGetAvailableStaticRuleCount(
+ extension,
+ availableStaticRulesCount - rules.ruleset_0.length,
+ "Got the available static rule count on ruleset_0 still the only one enabled"
+ );
+
+ await updateEnabledRulesets({
+ disableRulesetIds: ["ruleset_0"],
+ enableRulesetIds: ["ruleset_1"],
+ });
+
+ info("Expect ruleset_1 to be enabled along with disabling ruleset_0");
+ await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
+ delete expectedEnabledRulesets.ruleset_0;
+ expectedEnabledRulesets.ruleset_1 = getSchemaNormalizedRules(
+ extension,
+ rules.ruleset_1
+ );
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets, {
+ // Assert total amount of expected rules and only the first and last rule
+ // individually, to avoid generating a huge amount of logs and potential
+ // timeout failures on slower builds.
+ assertIndividualRules: false,
+ });
+
+ await assertDNRGetAvailableStaticRuleCount(
+ extension,
+ 0,
+ "Expect no additional static rules count available when ruleset_1 is enabled"
+ );
+
+ info(
+ "Expect ruleset_disabled to stay disabled because along with ruleset_1 exceeeds the limits"
+ );
+ await updateEnabledRulesets({
+ enableRulesetIds: ["ruleset_disabled"],
+ expectedErrorMessage: `Number of rules across all enabled static rulesets exceeds GUARANTEED_MINIMUM_STATIC_RULES if ruleset "ruleset_disabled" were to be enabled.`,
+ });
+ await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]);
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets, {
+ // Assert total amount of expected rules and only the first and last rule
+ // individually, to avoid generating a huge amount of logs and potential
+ // timeout failures on slower builds.
+ assertIndividualRules: false,
+ });
+ await assertDNRGetAvailableStaticRuleCount(
+ extension,
+ 0,
+ "Expect no additional static rules count available"
+ );
+
+ info("Expect ruleset_empty to be enabled despite having reached the limit");
+ await updateEnabledRulesets({
+ enableRulesetIds: ["ruleset_empty"],
+ });
+ await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_empty"]);
+ await assertDNRStoreData(
+ dnrStore,
+ extension,
+ {
+ ...expectedEnabledRulesets,
+ ruleset_empty: [],
+ },
+ // Assert total amount of expected rules and only the first and last rule
+ // individually, to avoid generating a huge amount of logs and potential
+ // timeout failures on slower builds.
+ { assertIndividualRules: false }
+ );
+ await assertDNRGetAvailableStaticRuleCount(
+ extension,
+ 0,
+ "Expect no additional static rules count available"
+ );
+
+ info("Expect invalid rules to not be counted towards the limits");
+ await updateEnabledRulesets({
+ disableRulesetIds: ["ruleset_1", "ruleset_empty"],
+ enableRulesetIds: ["ruleset_withInvalid"],
+ });
+ await assertDNRGetEnabledRulesets(extension, ["ruleset_withInvalid"]);
+ await assertDNRStoreData(dnrStore, extension, {
+ // Only the valid rule has been actually loaded, and the invalid one
+ // ignored.
+ ruleset_withInvalid: [ruleValid],
+ });
+ await assertDNRGetAvailableStaticRuleCount(
+ extension,
+ availableStaticRulesCount - 1,
+ "Expect only valid rules to be counted"
+ );
+
+ await extension.unload();
+
+ Services.prefs.clearUserPref("extensions.background.idle.enabled");
+});
+
+add_task(async function test_static_rulesets_limits() {
+ const dnrStore = ExtensionDNRStore._getStoreForTesting();
+
+ const getRulesetManifestData = (rulesetNumber, enabled) => {
+ return {
+ id: `ruleset_${rulesetNumber}`,
+ enabled,
+ path: `ruleset_${rulesetNumber}.json`,
+ };
+ };
+ const {
+ MAX_NUMBER_OF_STATIC_RULESETS,
+ MAX_NUMBER_OF_ENABLED_STATIC_RULESETS,
+ } = ExtensionDNRLimits;
+
+ equal(
+ typeof MAX_NUMBER_OF_STATIC_RULESETS,
+ "number",
+ "Expect MAX_NUMBER_OF_STATIC_RULESETS to be a number"
+ );
+ equal(
+ typeof MAX_NUMBER_OF_ENABLED_STATIC_RULESETS,
+ "number",
+ "Expect MAX_NUMBER_OF_ENABLED_STATIC_RULESETS to be a number"
+ );
+ ok(
+ MAX_NUMBER_OF_STATIC_RULESETS > MAX_NUMBER_OF_ENABLED_STATIC_RULESETS,
+ "Expect MAX_NUMBER_OF_STATIC_RULESETS to be greater"
+ );
+
+ const rules = [getDNRRule()];
+
+ const rule_resources = [];
+ const files = {};
+ for (let i = 0; i < MAX_NUMBER_OF_STATIC_RULESETS + 1; i++) {
+ const enabled = i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS + 1;
+ files[`ruleset_${i}.json`] = JSON.stringify(rules);
+ rule_resources.push(getRulesetManifestData(i, enabled));
+ }
+
+ let extension = ExtensionTestUtils.loadExtension(
+ getDNRExtension({
+ rule_resources,
+ files,
+ })
+ );
+
+ const expectedEnabledRulesets = {};
+
+ const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ await extension.startup();
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ await extension.awaitMessage("bgpage:ready");
+
+ for (let i = 0; i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS; i++) {
+ expectedEnabledRulesets[`ruleset_${i}`] = getSchemaNormalizedRules(
+ extension,
+ rules
+ );
+ }
+
+ await assertDNRGetEnabledRulesets(
+ extension,
+ Array.from(Object.keys(expectedEnabledRulesets))
+ );
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
+ });
+
+ AddonTestUtils.checkMessages(messages, {
+ expected: [
+ // Warnings emitted from the manifest schema validation.
+ {
+ message:
+ /declarative_net_request: Static rulesets are exceeding the MAX_NUMBER_OF_STATIC_RULESETS limit/,
+ },
+ {
+ message:
+ /declarative_net_request: Enabled static rulesets are exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS limit .* "ruleset_10"/,
+ },
+ // Error reported on the browser console as part of loading enabled rulesets)
+ // on enabled rulesets being ignored because exceeding the limit.
+ {
+ message:
+ /Ignoring enabled static ruleset exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS .* "ruleset_10"/,
+ },
+ ],
+ });
+
+ info(
+ "Verify updateEnabledRulesets reject when the request is exceeding the enabled rulesets count limit"
+ );
+ extension.sendMessage("updateEnabledRulesets", {
+ disableRulesetIds: ["ruleset_0"],
+ enableRulesetIds: ["ruleset_10", "ruleset_11"],
+ });
+
+ await Assert.rejects(
+ extension.awaitMessage("updateEnabledRulesets:done").then(results => {
+ if (results[0].rejectedWithErrorMessage) {
+ return Promise.reject(new Error(results[0].rejectedWithErrorMessage));
+ }
+ return results[0];
+ }),
+ /updatedEnabledRulesets request is exceeding MAX_NUMBER_OF_ENABLED_STATIC_RULESETS/,
+ "Expected rejection on updateEnabledRulesets exceeting enabled rulesets count limit"
+ );
+
+ // Confirm that the expected rulesets didn't change neither.
+ await assertDNRGetEnabledRulesets(
+ extension,
+ Array.from(Object.keys(expectedEnabledRulesets))
+ );
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
+
+ info(
+ "Verify updateEnabledRulesets applies the expected changes when resolves successfully"
+ );
+ extension.sendMessage(
+ "updateEnabledRulesets",
+ {
+ disableRulesetIds: ["ruleset_0"],
+ enableRulesetIds: ["ruleset_10"],
+ },
+ {
+ disableRulesetIds: ["ruleset_10"],
+ enableRulesetIds: ["ruleset_11"],
+ }
+ );
+ await extension.awaitMessage("updateEnabledRulesets:done");
+
+ // Expect ruleset_0 disabled, ruleset_10 to be enabled but then disabled by the
+ // second update queued after the first one, and ruleset_11 to be enabled.
+ delete expectedEnabledRulesets.ruleset_0;
+ expectedEnabledRulesets.ruleset_11 = getSchemaNormalizedRules(
+ extension,
+ rules
+ );
+
+ await assertDNRGetEnabledRulesets(
+ extension,
+ Array.from(Object.keys(expectedEnabledRulesets))
+ );
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
+
+ // Ensure all changes were stored and reloaded from disk store and the
+ // DNR store update queue can accept new updates.
+ info("Verify static rules load and updates after extension is restarted");
+
+ // 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 extension.awaitMessage("bgpage:ready");
+ await assertDNRGetEnabledRulesets(
+ extension,
+ Array.from(Object.keys(expectedEnabledRulesets))
+ );
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
+
+ extension.sendMessage("updateEnabledRulesets", {
+ disableRulesetIds: ["ruleset_11"],
+ });
+ await extension.awaitMessage("updateEnabledRulesets:done");
+ delete expectedEnabledRulesets.ruleset_11;
+ await assertDNRGetEnabledRulesets(
+ extension,
+ Array.from(Object.keys(expectedEnabledRulesets))
+ );
+ await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets);
+
+ 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();
+});