diff options
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.js | 1245 |
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(); +}); |