summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js1245
1 files changed, 1245 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js
new file mode 100644
index 0000000000..4ba120852f
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_dynamic_rules.js
@@ -0,0 +1,1245 @@
+/* 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();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "43"
+);
+
+Services.scriptloader.loadSubScript(
+ Services.io.newFileURI(do_get_file("head_dnr.js")).spec,
+ this
+);
+
+const { promiseStartupManager, promiseRestartManager } = AddonTestUtils;
+
+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 promiseStartupManager();
+});
+
+// This function is serialized and called in the context of the test extension's
+// background page. dnrTestUtils is passed to the background function.
+function makeDnrTestUtils() {
+ const dnrTestUtils = {};
+ const dnr = browser.declarativeNetRequest;
+
+ function serializeForLog(data) {
+ // JSON-stringify, but drop null values (replacing them with undefined
+ // causes JSON.stringify to drop them), so that optional keys with the null
+ // values are hidden.
+ let str = JSON.stringify(data, rep => rep ?? undefined);
+ return str;
+ }
+
+ async function testInvalidRule(rule, expectedError, isSchemaError) {
+ if (isSchemaError) {
+ // Schema validation error = thrown error instead of a rejection.
+ browser.test.assertThrows(
+ () => dnr.updateDynamicRules({ addRules: [rule] }),
+ expectedError,
+ `Rule should be invalid (schema-validated): ${serializeForLog(rule)}`
+ );
+ } else {
+ await browser.test.assertRejects(
+ dnr.updateDynamicRules({ addRules: [rule] }),
+ expectedError,
+ `Rule should be invalid: ${serializeForLog(rule)}`
+ );
+ }
+ }
+
+ Object.assign(dnrTestUtils, {
+ testInvalidRule,
+ serializeForLog,
+ });
+ return dnrTestUtils;
+}
+
+async function runAsDNRExtension({
+ background,
+ unloadTestAtEnd = true,
+ awaitFinish = false,
+ id = "test-dynamic-rules@test-extension",
+}) {
+ const testExtensionParams = {
+ background: `(${background})((${makeDnrTestUtils})())`,
+ useAddonManager: "permanent",
+ manifest: {
+ manifest_version: 3,
+ permissions: ["declarativeNetRequest", "declarativeNetRequestFeedback"],
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ },
+ };
+ const extension = ExtensionTestUtils.loadExtension(testExtensionParams);
+ await extension.startup();
+ if (awaitFinish) {
+ await extension.awaitFinish();
+ }
+ if (unloadTestAtEnd) {
+ await extension.unload();
+ }
+ return { extension, testExtensionParams };
+}
+
+function callTestMessageHandler(extension, testMessage, ...args) {
+ extension.sendMessage(testMessage, ...args);
+ return extension.awaitMessage(`${testMessage}:done`);
+}
+
+add_task(async function test_dynamic_rule_registration() {
+ await runAsDNRExtension({
+ background: async () => {
+ const dnr = browser.declarativeNetRequest;
+
+ await dnr.updateDynamicRules({
+ addRules: [{ id: 1, condition: {}, action: { type: "block" } }],
+ });
+
+ const url = "https://example.com/some-dummy-url";
+ const type = "font";
+ browser.test.assertDeepEq(
+ { matchedRules: [{ ruleId: 1, rulesetId: "_dynamic" }] },
+ await dnr.testMatchOutcome({ url, type }),
+ "Dynamic rule matched after registration"
+ );
+
+ await dnr.updateDynamicRules({
+ removeRuleIds: [
+ 1,
+ 1234567890, // Invalid rules should be ignored.
+ ],
+ addRules: [{ id: 2, condition: {}, action: { type: "block" } }],
+ });
+ browser.test.assertDeepEq(
+ { matchedRules: [{ ruleId: 2, rulesetId: "_dynamic" }] },
+ await dnr.testMatchOutcome({ url, type }),
+ "Dynamic rule matched after update"
+ );
+
+ await dnr.updateDynamicRules({ removeRuleIds: [2] });
+ browser.test.assertDeepEq(
+ { matchedRules: [] },
+ await dnr.testMatchOutcome({ url, type }),
+ "Dynamic rule not matched after unregistration"
+ );
+
+ browser.test.notifyPass();
+ },
+ });
+});
+
+add_task(async function test_dynamic_rules_count_limits() {
+ await runAsDNRExtension({
+ unloadTestAtEnd: true,
+ awaitFinish: true,
+ background: async () => {
+ const dnr = browser.declarativeNetRequest;
+ const [dyamicRules, sessionRules] = await Promise.all([
+ dnr.getDynamicRules(),
+ dnr.getSessionRules(),
+ ]);
+
+ browser.test.assertDeepEq(
+ { session: [], dynamic: [] },
+ { session: sessionRules, dynamic: dyamicRules },
+ "Expect no session and no dynamic rules"
+ );
+
+ const { MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES } = dnr;
+ const DUMMY_RULE = {
+ action: { type: "block" },
+ condition: { resourceTypes: ["main_frame"] },
+ };
+ const rules = [];
+ for (let i = 0; i < MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES; i++) {
+ rules.push({ ...DUMMY_RULE, id: i + 1 });
+ }
+
+ await browser.test.assertRejects(
+ dnr.updateDynamicRules({
+ addRules: [
+ ...rules,
+ { ...DUMMY_RULE, id: MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES + 1 },
+ ],
+ }),
+ `Number of rules in ruleset "_dynamic" exceeds MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES.`,
+ "Got the expected rejection of exceeding the number of dynamic rules allowed"
+ );
+
+ await dnr.updateDynamicRules({
+ addRules: rules,
+ });
+ browser.test.assertEq(
+ 5000,
+ (await dnr.getDynamicRules()).length,
+ "Got the expected number of dynamic rules stored"
+ );
+
+ await dnr.updateDynamicRules({
+ removeRuleIds: rules.map(r => r.id),
+ });
+
+ browser.test.assertEq(
+ 0,
+ (await dnr.getDynamicRules()).length,
+ "All dynamic rules should have been removed"
+ );
+
+ browser.test.log(
+ "Verify rules count limits with multiple async API calls"
+ );
+
+ const [updateDynamicRulesSingle, updateDynamicRulesTooMany] =
+ await Promise.allSettled([
+ dnr.updateDynamicRules({
+ addRules: [
+ {
+ ...DUMMY_RULE,
+ id: MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES + 1,
+ },
+ ],
+ }),
+ dnr.updateDynamicRules({ addRules: rules }),
+ ]);
+
+ browser.test.assertDeepEq(
+ updateDynamicRulesSingle,
+ { status: "fulfilled", value: undefined },
+ "Expect the first updateDynamicRules call to be successful"
+ );
+
+ await browser.test.assertRejects(
+ updateDynamicRulesTooMany?.status === "rejected"
+ ? Promise.reject(updateDynamicRulesTooMany.reason)
+ : Promise.resolve(),
+ `Number of rules in ruleset "_dynamic" exceeds MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES.`,
+ "Got the expected rejection on the second call exceeding the number of dynamic rules allowed"
+ );
+
+ browser.test.assertDeepEq(
+ (await dnr.getDynamicRules()).map(rule => rule.id),
+ [MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES + 1],
+ "Got the expected dynamic rules"
+ );
+
+ await dnr.updateDynamicRules({
+ removeRuleIds: [MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES + 1],
+ });
+
+ const [updateSessionResult, updateDynamicResult] =
+ await Promise.allSettled([
+ dnr.updateSessionRules({ addRules: rules }),
+ dnr.updateDynamicRules({ addRules: rules }),
+ ]);
+
+ browser.test.assertDeepEq(
+ updateDynamicResult,
+ { status: "fulfilled", value: undefined },
+ "Expect the number of dynamic rules to be still allowed, despite the session rule added"
+ );
+
+ // NOTE: In this test we do not exceed the quota of session rules. The
+ // updateSessionRules call here is to verify that the quota of session and
+ // dynamic rules are separate. The limits for session rules are tested
+ // by session_rules_total_rule_limit in test_ext_dnr_session_rules.js.
+ browser.test.assertDeepEq(
+ updateSessionResult,
+ { status: "fulfilled", value: undefined },
+ "Got expected success from the updateSessionRules request"
+ );
+
+ browser.test.assertDeepEq(
+ { sessionRulesCount: 5000, dynamicRulesCount: 5000 },
+ {
+ sessionRulesCount: (await dnr.getSessionRules()).length,
+ dynamicRulesCount: (await dnr.getDynamicRules()).length,
+ },
+ "Got expected session and dynamic rules counts"
+ );
+
+ browser.test.notifyPass();
+ },
+ });
+});
+
+add_task(async function test_stored_dynamic_rules_exceeding_limits() {
+ const { extension } = await runAsDNRExtension({
+ unloadTestAtEnd: false,
+ awaitFinish: false,
+ background: async () => {
+ const dnr = browser.declarativeNetRequest;
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "createDynamicRules": {
+ const [{ updateRuleOptions }] = args;
+ await dnr.updateDynamicRules(updateRuleOptions);
+ break;
+ }
+ case "assertGetDynamicRulesCount": {
+ const [{ expectedRulesCount }] = args;
+ browser.test.assertEq(
+ expectedRulesCount,
+ (await dnr.getDynamicRules()).length,
+ "getDynamicRules() resolves to the expected number of dynamic rules"
+ );
+ break;
+ }
+ default:
+ browser.test.fail(
+ `Got unexpected unhandled test message: "${msg}"`
+ );
+ break;
+ }
+ browser.test.sendMessage(`${msg}:done`);
+ });
+ browser.test.sendMessage("bgpage:ready");
+ },
+ });
+
+ const initialRules = [getDNRRule({ id: 1 })];
+ await extension.awaitMessage("bgpage:ready");
+ await callTestMessageHandler(extension, "createDynamicRules", {
+ updateRuleOptions: { addRules: initialRules },
+ });
+ await callTestMessageHandler(extension, "assertGetDynamicRulesCount", {
+ expectedRulesCount: 1,
+ });
+
+ const extUUID = extension.uuid;
+ const dnrStore = ExtensionDNRStore._getStoreForTesting();
+ await dnrStore._savePromises.get(extUUID);
+ const { storeFile } = dnrStore.getFilePaths(extUUID);
+
+ 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"
+ );
+
+ ok(await IOUtils.exists(storeFile), `DNR storeFile ${storeFile} found`);
+ const dnrDataFromFile = await IOUtils.readJSON(storeFile, {
+ decompress: true,
+ });
+
+ const { MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES } = ExtensionDNRLimits;
+
+ const expectedDynamicRules = [];
+ const unexpectedDynamicRules = [];
+
+ for (let i = 0; i < MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES + 5; i++) {
+ const rule = getDNRRule({ id: i + 1 });
+ if (i < MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES) {
+ expectedDynamicRules.push(rule);
+ } else {
+ unexpectedDynamicRules.push(rule);
+ }
+ }
+
+ const tooManyDynamicRules = [
+ ...expectedDynamicRules,
+ ...unexpectedDynamicRules,
+ ];
+
+ const dnrDataNew = {
+ schemaVersion: dnrDataFromFile.schemaVersion,
+ extVersion: extension.extension.version,
+ staticRulesets: [],
+ dynamicRuleset: getSchemaNormalizedRules(extension, tooManyDynamicRules),
+ };
+
+ await IOUtils.writeJSON(storeFile, dnrDataNew, { compress: true });
+
+ const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ await extension.addon.enable();
+ await extension.awaitMessage("bgpage:ready");
+ });
+
+ await callTestMessageHandler(extension, "assertGetDynamicRulesCount", {
+ expectedRulesCount: 0,
+ });
+
+ AddonTestUtils.checkMessages(messages, {
+ expected: [
+ {
+ message: new RegExp(
+ `Ignoring dynamic ruleset in extension "${extension.id}" because: Number of rules in ruleset "_dynamic" exceeds MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES`
+ ),
+ },
+ ],
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_save_and_load_dynamic_rules() {
+ let { extension, testExtensionParams } = await runAsDNRExtension({
+ unloadTestAtEnd: false,
+ awaitFinish: false,
+ background: async dnrTestUtils => {
+ const dnr = browser.declarativeNetRequest;
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "assertGetDynamicRules": {
+ const [{ expectedRules }] = args;
+ browser.test.assertDeepEq(
+ expectedRules,
+ await dnr.getDynamicRules(),
+ "getDynamicRules() resolves to the expected dynamic rules"
+ );
+ break;
+ }
+ case "testUpdateDynamicRules": {
+ const [{ updateRulesRequests, expectedRules }] = args;
+ const promiseResults = await Promise.allSettled(
+ updateRulesRequests.map(updateRuleOptions =>
+ dnr.updateDynamicRules(updateRuleOptions)
+ )
+ );
+
+ // All calls should have been resolved successfully.
+ for (const [i, request] of updateRulesRequests.entries()) {
+ browser.test.assertDeepEq(
+ { status: "fulfilled", value: undefined },
+ promiseResults[i],
+ `Expect resolved updateDynamicRules request for ${dnrTestUtils.serializeForLog(
+ request
+ )}`
+ );
+ }
+
+ browser.test.assertDeepEq(
+ expectedRules,
+ await dnr.getDynamicRules(),
+ "getDynamicRules resolves to the expected updated dynamic rules"
+ );
+ break;
+ }
+ case "testInvalidDynamicAddRule": {
+ const [{ rule, expectedError, isSchemaError, isErrorRegExp }] =
+ args;
+ await dnrTestUtils.testInvalidRule(
+ rule,
+ expectedError,
+ isSchemaError,
+ isErrorRegExp
+ );
+ break;
+ }
+ default:
+ browser.test.fail(
+ `Got unexpected unhandled test message: "${msg}"`
+ );
+ break;
+ }
+
+ browser.test.sendMessage(`${msg}:done`);
+ });
+
+ browser.test.sendMessage("bgpage:ready");
+ },
+ });
+
+ await extension.awaitMessage("bgpage:ready");
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: [],
+ });
+
+ const rules = [
+ getDNRRule({
+ id: 1,
+ action: { type: "allow" },
+ condition: { resourceTypes: ["main_frame"] },
+ }),
+ getDNRRule({
+ id: 2,
+ action: { type: "block" },
+ condition: { resourceTypes: ["main_frame", "script"] },
+ }),
+ ];
+
+ info("Verify updateDynamicRules adding new valid rules");
+ // Send two concurrent API requests, the first one adds 3 rules and the second
+ // one removing a rule defined in the first call, the result of the combined
+ // API calls is expected to only store 2 dynamic rules in the DNR store.
+ await callTestMessageHandler(extension, "testUpdateDynamicRules", {
+ updateRulesRequests: [
+ { addRules: [...rules, getDNRRule({ id: 3 })] },
+ { removeRuleIds: [3] },
+ ],
+ expectedRules: getSchemaNormalizedRules(extension, rules),
+ });
+
+ const extUUID = extension.uuid;
+ const dnrStore = ExtensionDNRStore._getStoreForTesting();
+ await dnrStore._savePromises.get(extUUID);
+ const { storeFile } = dnrStore.getFilePaths(extUUID);
+ const dnrDataFromFile = await IOUtils.readJSON(storeFile, {
+ decompress: true,
+ });
+
+ Assert.deepEqual(
+ dnrDataFromFile.dynamicRuleset,
+ getSchemaNormalizedRules(extension, rules),
+ "Got the expected rules stored on disk"
+ );
+
+ info("Verify updateDynamicRules rejects on new invalid rules");
+ await callTestMessageHandler(extension, "testInvalidDynamicAddRule", {
+ rule: rules[0],
+ expectedError: "Duplicate rule ID: 1",
+ isSchemaError: false,
+ });
+
+ await callTestMessageHandler(extension, "testInvalidDynamicAddRule", {
+ rule: getDNRRule({ action: { type: "invalid-action" } }),
+ expectedError:
+ /addRules.0.action.type: Invalid enumeration value "invalid-action"/,
+ isSchemaError: true,
+ });
+
+ info("Expect dynamic rules to not have been changed");
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, rules),
+ });
+
+ Assert.deepEqual(
+ dnrStore._data.get(extUUID).dynamicRuleset,
+ getSchemaNormalizedRules(extension, rules),
+ "Got the expected dynamic rules in the DNR store"
+ );
+
+ info("Verify dynamic rules loaded back from disk on addon restart");
+ 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);
+
+ const { addon } = extension;
+ 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");
+
+ info("Expect dynamic rules to have been loaded back");
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, rules),
+ });
+
+ Assert.deepEqual(
+ dnrStore._data.get(extUUID).dynamicRuleset,
+ getSchemaNormalizedRules(extension, rules),
+ "Got the expected dynamic rules loaded back from the DNR store after addon restart"
+ );
+
+ info("Verify dynamic rules loaded back as expected on AOM restart");
+ dnrStore._data.delete(extUUID);
+ dnrStore._dataPromises.delete(extUUID);
+
+ // 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 promiseRestartManager();
+
+ await extension.awaitStartup();
+ await extension.awaitMessage("bgpage:ready");
+
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, rules),
+ });
+
+ // Verify the dynamic rules are converted back into Rule class instances
+ // as expected when loaded back from the DNR store file
+ Assert.ok(
+ !!dnrStore._data.get(extUUID).dynamicRuleset.length,
+ "Expected dynamic rules to have been loaded back from the DNR store file"
+ );
+ Assert.deepEqual(
+ dnrStore._data
+ .get(extUUID)
+ .dynamicRuleset.filter(rule => rule.constructor.name !== "Rule"),
+ [],
+ "Expect dynamic rules loaded back from the DNR store file to be converted to Rule class instances"
+ );
+
+ Assert.deepEqual(
+ dnrStore._data.get(extUUID).dynamicRuleset,
+ getSchemaNormalizedRules(extension, rules),
+ "Got the expected dynamic rules loaded back from the DNR store after AOM restart"
+ );
+
+ info(
+ "Verify updateDynamicRules adding new valid rules and remove one of the existing"
+ );
+ // Expect the first rule to be removed and a new one being added.
+ const newRule3 = getDNRRule({
+ id: 3,
+ action: { type: "allow" },
+ condition: { resourceTypes: ["main_frame"] },
+ });
+ const updatedRules = [rules[1], newRule3];
+
+ await callTestMessageHandler(extension, "testUpdateDynamicRules", {
+ updateRulesRequests: [{ addRules: [newRule3], removeRuleIds: [1] }],
+ expectedRules: getSchemaNormalizedRules(extension, updatedRules),
+ });
+
+ info("Verify dynamic rules preserved across addon updates");
+
+ const staticRules = [
+ getDNRRule({
+ id: 4,
+ action: { type: "block" },
+ condition: { resourceTypes: ["xmlhttprequest"] },
+ }),
+ ];
+ await extension.upgrade({
+ ...testExtensionParams,
+ manifest: {
+ ...testExtensionParams.manifest,
+ version: "2.0",
+ declarative_net_request: {
+ rule_resources: [
+ {
+ id: "ruleset_1",
+ enabled: true,
+ path: "ruleset_1.json",
+ },
+ ],
+ },
+ },
+ files: { "ruleset_1.json": JSON.stringify(staticRules) },
+ });
+ await extension.awaitMessage("bgpage:ready");
+
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, updatedRules),
+ });
+
+ info(
+ "Verify static rules included in the new addon version have been loaded"
+ );
+
+ await assertDNRStoreData(dnrStore, extension, {
+ ruleset_1: getSchemaNormalizedRules(extension, staticRules),
+ });
+
+ info("Verify rules after extension downgrade");
+ await extension.upgrade({
+ ...testExtensionParams,
+ manifest: {
+ ...testExtensionParams.manifest,
+ version: "1.0",
+ },
+ });
+ await extension.awaitMessage("bgpage:ready");
+
+ info("Verify stored dynamic rules are unchanged");
+
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, updatedRules),
+ });
+
+ info(
+ "Verify static rules included in the new addon version are cleared on downgrade to previous version"
+ );
+ await assertDNRStoreData(dnrStore, extension, {});
+
+ info("Verify rules after extension upgrade to one without DNR permissions");
+ await extension.upgrade({
+ ...testExtensionParams,
+ manifest: {
+ ...testExtensionParams.manifest,
+ permissions: [],
+ version: "1.1",
+ },
+ background: async () => {
+ browser.test.assertEq(
+ browser.declarativeNetRequest,
+ undefined,
+ "Expect DNR API namespace to not be available"
+ );
+ browser.test.sendMessage("bgpage:ready");
+ },
+ });
+ await extension.awaitMessage("bgpage:ready");
+ ok(
+ !dnrStore._dataPromises.has(extension.uuid),
+ "Expect dnrStore to not have any promise for the extension DNR data being loaded"
+ );
+ ok(
+ !ExtensionDNR.getRuleManager(
+ extension.extension,
+ false /* createIfMissing */
+ ),
+ "Expect no ruleManager found for the extenson"
+ );
+
+ info(
+ "Verify rules are loaded back after upgrading again to one with DNR permissions"
+ );
+ await extension.upgrade({
+ ...testExtensionParams,
+ manifest: {
+ ...testExtensionParams.manifest,
+ version: "1.2",
+ },
+ });
+ await extension.awaitMessage("bgpage:ready");
+
+ // NOTE: To make sure that the test extension rule manager is removed
+ // on the extension shutdown also when the declarativeNetRequest
+ // ExtensionAPI class instance has not been created at all, this part
+ // on the test is purposely not calling any declarativeNetRequest API method
+ // not calling ExtensionDNR.ensureInitialized, instead we wait for the
+ // RuleManager instance to be created and then we disable the
+ // test extension and assert that the RuleManager has been cleared.
+ let ruleManager = await TestUtils.waitForCondition(
+ () =>
+ ExtensionDNR.getRuleManager(
+ extension.extension,
+ /* createIfMissing= */ false
+ ),
+ "Wait for the test extension RuleManager to have neem created"
+ );
+ Assert.ok(ruleManager, "Rule manager exists before unload");
+ Assert.deepEqual(
+ ruleManager.getDynamicRules(),
+ getSchemaNormalizedRules(extension, updatedRules),
+ "Found the expected dynamic rules in the Rule manager"
+ );
+ await extension.addon.disable();
+ Assert.ok(
+ !ExtensionDNR.getRuleManager(
+ extension.extension,
+ /* createIfMissing= */ false
+ ),
+ "Rule manager erased after unload"
+ );
+
+ await extension.addon.enable();
+ await extension.awaitMessage("bgpage:ready");
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: getSchemaNormalizedRules(extension, updatedRules),
+ });
+
+ info("Verify dynamic rules updates after corrupted storage");
+
+ async function testLoadedRulesAfterDataCorruption({
+ name,
+ asyncWriteStoreFile,
+ expectedCorruptFile,
+ }) {
+ info(`Tampering DNR store data: ${name}`);
+
+ await extension.addon.disable();
+ Assert.ok(
+ !ExtensionDNR.getRuleManager(
+ extension.extension,
+ /* createIfMissing= */ false
+ ),
+ "Rule manager erased after unload"
+ );
+
+ 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 asyncWriteStoreFile();
+
+ await extension.addon.enable();
+ await extension.awaitMessage("bgpage:ready");
+
+ await TestUtils.waitForCondition(
+ () => IOUtils.exists(`${expectedCorruptFile}`),
+ `Wait for the "${expectedCorruptFile}" file to have been created`
+ );
+
+ ok(
+ !(await IOUtils.exists(storeFile)),
+ "Corrupted store file expected to be removed"
+ );
+
+ await callTestMessageHandler(extension, "assertGetDynamicRules", {
+ expectedRules: [],
+ });
+
+ const newRules = [getDNRRule({ id: 3 })];
+ const expectedRules = getSchemaNormalizedRules(extension, newRules);
+ await callTestMessageHandler(extension, "testUpdateDynamicRules", {
+ updateRulesRequests: [{ addRules: newRules }],
+ expectedRules,
+ });
+
+ await TestUtils.waitForCondition(
+ () => IOUtils.exists(storeFile),
+ `Wait for the "${storeFile}" file to have been created`
+ );
+
+ const newData = await IOUtils.readJSON(storeFile, { decompress: true });
+ Assert.deepEqual(
+ newData.dynamicRuleset,
+ expectedRules,
+ "Expect the new rules to have been stored on disk"
+ );
+ }
+
+ 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: dnrDataFromFile.schemaVersion,
+ extVersion: extension.extension.version,
+ staticRulesets: "Not an array",
+ }),
+ { compress: true }
+ ),
+ expectedCorruptFile: `${storeFile}-3.corrupt`,
+ });
+
+ await testLoadedRulesAfterDataCorruption({
+ name: "invalid dynamicRuleset property type",
+ asyncWriteStoreFile: () =>
+ IOUtils.writeUTF8(
+ storeFile,
+ JSON.stringify({
+ schemaVersion: dnrDataFromFile.schemaVersion,
+ extVersion: extension.extension.version,
+ staticRulesets: [],
+ dynamicRuleset: "Not an array",
+ }),
+ { compress: true }
+ ),
+ expectedCorruptFile: `${storeFile}-4.corrupt`,
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_tabId_conditions_invalid_in_dynamic_rules() {
+ await runAsDNRExtension({
+ unloadTestAtEnd: true,
+ awaitFinish: true,
+ background: async dnrTestUtils => {
+ await dnrTestUtils.testInvalidRule(
+ { id: 1, action: { type: "block" }, condition: { tabIds: [1] } },
+ "tabIds and excludedTabIds can only be specified in session rules"
+ );
+ await dnrTestUtils.testInvalidRule(
+ {
+ id: 1,
+ action: { type: "block" },
+ condition: { excludedTabIds: [1] },
+ },
+ "tabIds and excludedTabIds can only be specified in session rules"
+ );
+ browser.test.assertDeepEq(
+ [],
+ await browser.declarativeNetRequest.getDynamicRules(),
+ "Expect the invalid rules to not be enabled"
+ );
+ browser.test.notifyPass();
+ },
+ });
+});
+
+add_task(async function test_dynamic_rules_telemetry() {
+ resetTelemetryData();
+
+ let { extension } = await runAsDNRExtension({
+ unloadTestAtEnd: false,
+ awaitFinish: false,
+ id: "test-dynamic-rules-telemetry@test-extension",
+ background: () => {
+ const dnr = browser.declarativeNetRequest;
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "getDynamicRules": {
+ browser.test.sendMessage(
+ `${msg}:done`,
+ await dnr.getDynamicRules()
+ );
+ break;
+ }
+ case "updateDynamicRules": {
+ const { addRules, removeRuleIds } = args[0];
+ await dnr.updateDynamicRules({
+ addRules,
+ removeRuleIds,
+ });
+ browser.test.sendMessage(
+ `${msg}:done`,
+ await dnr.getDynamicRules()
+ );
+ break;
+ }
+ default: {
+ browser.test.fail(`Unexpected test message: ${msg}`);
+ browser.test.sendMessage(`${msg}:done`);
+ break;
+ }
+ }
+ });
+ browser.test.sendMessage("bgpage:ready");
+ },
+ });
+
+ await extension.awaitMessage("bgpage:ready");
+
+ extension.sendMessage("getDynamicRules");
+ Assert.deepEqual(
+ await extension.awaitMessage("getDynamicRules:done"),
+ [],
+ "Expect no dynamic DNR rules"
+ );
+
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ },
+ ],
+ "before test extension have been loaded"
+ );
+
+ const dynamicRules = [
+ getDNRRule({
+ id: 1,
+ action: { type: "block" },
+ condition: {
+ resourceTypes: ["xmlhttprequest"],
+ requestDomains: ["example.com"],
+ },
+ }),
+ getDNRRule({
+ id: 2,
+ action: { type: "block" },
+ condition: {
+ resourceTypes: ["xmlhttprequest"],
+ requestDomains: ["example.org"],
+ },
+ }),
+ ];
+
+ await extension.sendMessage("updateDynamicRules", {
+ addRules: dynamicRules,
+ });
+
+ Assert.deepEqual(
+ await extension.awaitMessage("updateDynamicRules:done"),
+ getSchemaNormalizedRules(extension, dynamicRules),
+ "Expect new dynamic DNR rules to have been added"
+ );
+
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ },
+ ],
+ "no additional rule validation expected for dynamic rules pre-validated on a updateDynamicRules API call"
+ );
+
+ extension.sendMessage("updateDynamicRules", {
+ removeRuleIds: [dynamicRules[1].id],
+ });
+
+ Assert.deepEqual(
+ await extension.awaitMessage("updateDynamicRules:done"),
+ getSchemaNormalizedRules(extension, [dynamicRules[0]]),
+ `Expect dynamic DNR rule with id ${dynamicRules[1].id} to have been removed`
+ );
+
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ },
+ ],
+ "no additional rule validation expected for dynamic rules removed by a updateDynamicRules API call"
+ );
+
+ info("Disabling test extension");
+ await extension.addon.disable();
+
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ },
+ ],
+ "no rule validation hit after disabling the extension"
+ );
+
+ info("Re-enabling test extension");
+ await extension.addon.enable();
+ await extension.awaitMessage("bgpage:ready");
+ info(
+ "Wait for DNR initialization completed for the re-enabled permanently installed extension"
+ );
+ await ExtensionDNR.ensureInitialized(extension.extension);
+
+ assertDNRTelemetryMetricsSamplesCount(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ expectedSamplesCount: 1,
+ },
+ ],
+ "expected rule validation to be hit on re-loading dynamic rules from DNR store file"
+ );
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ // Expected no startup cache file to be loaded or used on re-enabling a disabled extension.
+ {
+ metric: "startupCacheReadSize",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_READ_BYTES",
+ mirroredType: "histogram",
+ },
+ {
+ metric: "startupCacheReadTime",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_READ_MS",
+ mirroredType: "histogram",
+ },
+ ],
+ "on loading dnr rules for newly installed extension"
+ );
+
+ info("Verify evaluateRulesCountMax telemetry probe");
+
+ 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 currently included in the dynamic ruleset.
+ let expectedEvaluateRulesCountMax = 1;
+ assertDNRTelemetryMetricsGetValueEq(
+ [
+ {
+ metric: "evaluateRulesCountMax",
+ mirroredName: "extensions.apis.dnr.evaluate_rules_count_max",
+ mirroredType: "scalar",
+ expectedGetValue: expectedEvaluateRulesCountMax,
+ },
+ ],
+ "evaluateRulesCountMax should be collected after evaluated dynamic rulesets"
+ );
+
+ extension.sendMessage("updateDynamicRules", {
+ addRules: [dynamicRules[1]],
+ });
+
+ Assert.deepEqual(
+ await extension.awaitMessage("updateDynamicRules:done"),
+ getSchemaNormalizedRules(extension, dynamicRules),
+ `Expect second dynamic DNR rules to have been added`
+ );
+
+ 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"
+ );
+
+ // Expect new number of rules currently included in the dynamic ruleset.
+ expectedEvaluateRulesCountMax = dynamicRules.length;
+ assertDNRTelemetryMetricsGetValueEq(
+ [
+ {
+ metric: "evaluateRulesCountMax",
+ mirroredName: "extensions.apis.dnr.evaluate_rules_count_max",
+ mirroredType: "scalar",
+ expectedGetValue: expectedEvaluateRulesCountMax,
+ },
+ ],
+ "evaluateRulesCountMax should be increased after evaluated two dynamic rules"
+ );
+
+ extension.sendMessage("updateDynamicRules", {
+ removeRuleIds: [dynamicRules[1].id],
+ });
+
+ await callPageFetch();
+
+ Assert.deepEqual(
+ await extension.awaitMessage("updateDynamicRules:done"),
+ getSchemaNormalizedRules(extension, [dynamicRules[0]]),
+ `Expect only first dynamic DNR rule to have be available`
+ );
+
+ assertDNRTelemetryMetricsGetValueEq(
+ [
+ {
+ metric: "evaluateRulesCountMax",
+ mirroredName: "extensions.apis.dnr.evaluate_rules_count_max",
+ mirroredType: "scalar",
+ expectedGetValue: expectedEvaluateRulesCountMax,
+ },
+ ],
+ "evaluateRulesCountMax should NOT be decreased after removing one dynamic rules"
+ );
+
+ await page.close();
+
+ await extension.unload();
+});