/* 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(); });