diff options
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js')
-rw-r--r-- | toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js | 1850 |
1 files changed, 1850 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js new file mode 100644 index 0000000000..4d20bd330e --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_static_rules.js @@ -0,0 +1,1850 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs", + ExtensionDNRLimits: "resource://gre/modules/ExtensionDNRLimits.sys.mjs", + ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); + +Services.scriptloader.loadSubScript( + Services.io.newFileURI(do_get_file("head_dnr.js")).spec, + this +); + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler("/", (req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.write("response from server"); +}); + +function backgroundWithDNRAPICallHandlers() { + browser.test.onMessage.addListener(async (msg, ...args) => { + let result; + switch (msg) { + case "getEnabledRulesets": + result = await browser.declarativeNetRequest.getEnabledRulesets(); + break; + case "getAvailableStaticRuleCount": + result = + await browser.declarativeNetRequest.getAvailableStaticRuleCount(); + break; + case "testMatchOutcome": + result = await browser.declarativeNetRequest + .testMatchOutcome(...args) + .catch(err => + browser.test.fail( + `Unexpected rejection from testMatchOutcome call: ${err}` + ) + ); + break; + case "updateEnabledRulesets": + // Run (one or more than one concurrently) updateEnabledRulesets calls + // and report back the results. + result = await Promise.all( + args.map(arg => { + return browser.declarativeNetRequest + .updateEnabledRulesets(arg) + .catch(err => { + return { rejectedWithErrorMessage: err.message }; + }); + }) + ); + break; + default: + browser.test.fail(`Unexpected test message: ${msg}`); + return; + } + + browser.test.sendMessage(`${msg}:done`, result); + }); + + browser.test.sendMessage("bgpage:ready"); +} + +function getDNRExtension({ + id = "test-dnr-static-rules@test-extension", + version = "1.0", + background = backgroundWithDNRAPICallHandlers, + useAddonManager = "permanent", + rule_resources, + declarative_net_request, + files, +}) { + // Omit declarative_net_request if rule_resources isn't defined + // (because declarative_net_request fails the manifest validation + // if rule_resources is missing). + const dnr = rule_resources ? { rule_resources } : undefined; + + return { + background, + useAddonManager, + manifest: { + manifest_version: 3, + version, + permissions: ["declarativeNetRequest", "declarativeNetRequestFeedback"], + // Needed to make sure the upgraded extension will have the same id and + // same uuid (which is mapped based on the extension id). + browser_specific_settings: { + gecko: { id }, + }, + declarative_net_request: declarative_net_request + ? { ...declarative_net_request, ...(dnr ?? {}) } + : dnr, + }, + files, + }; +} + +const assertDNRTestMatchOutcome = async ( + { extension, testRequest, expected }, + assertMessage +) => { + extension.sendMessage("testMatchOutcome", testRequest); + Assert.deepEqual( + expected, + await extension.awaitMessage("testMatchOutcome:done"), + assertMessage ?? + "Got the expected matched rules from testMatchOutcome API call" + ); +}; + +const assertDNRGetAvailableStaticRuleCount = async ( + extensionTestWrapper, + expectedCount, + assertMessage +) => { + extensionTestWrapper.sendMessage("getAvailableStaticRuleCount"); + Assert.deepEqual( + await extensionTestWrapper.awaitMessage("getAvailableStaticRuleCount:done"), + expectedCount, + assertMessage ?? + "Got the expected count value from dnr.getAvailableStaticRuleCount API method" + ); +}; + +const assertDNRGetEnabledRulesets = async ( + extensionTestWrapper, + expectedRulesetIds +) => { + extensionTestWrapper.sendMessage("getEnabledRulesets"); + Assert.deepEqual( + await extensionTestWrapper.awaitMessage("getEnabledRulesets:done"), + expectedRulesetIds, + "Got the expected enabled ruleset ids from dnr.getEnabledRulesets API method" + ); +}; + +add_setup(async () => { + Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); + Services.prefs.setBoolPref("extensions.dnr.enabled", true); + Services.prefs.setBoolPref("extensions.dnr.feedback", true); + + setupTelemetryForTests(); + + await ExtensionTestUtils.startAddonManager(); +}); + +add_task(async function test_load_static_rules() { + const ruleset1Data = [ + getDNRRule({ + action: { type: "allow" }, + condition: { resourceTypes: ["main_frame"] }, + }), + ]; + const ruleset2Data = [ + getDNRRule({ + action: { type: "block" }, + condition: { resourceTypes: ["main_frame", "script"] }, + }), + ]; + + const rule_resources = [ + { + id: "ruleset_1", + enabled: true, + path: "ruleset_1.json", + }, + { + id: "ruleset_2", + enabled: true, + path: "ruleset_2.json", + }, + { + id: "ruleset_3", + enabled: false, + path: "ruleset_3.json", + }, + ]; + const files = { + // Missing ruleset_3.json on purpose. + "ruleset_1.json": JSON.stringify(ruleset1Data), + "ruleset_2.json": JSON.stringify(ruleset2Data), + }; + + const extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ rule_resources, files }) + ); + + await extension.startup(); + + const extUUID = extension.uuid; + + await extension.awaitMessage("bgpage:ready"); + + const dnrStore = ExtensionDNRStore._getStoreForTesting(); + + info("Verify DNRStore data for the test extension"); + await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]); + + await assertDNRStoreData(dnrStore, extension, { + ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), + ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), + }); + + info("Verify matched rules using testMatchOutcome"); + const testRequestMainFrame = { + url: "https://example.com/some-dummy-url", + type: "main_frame", + }; + const testRequestScript = { + url: "https://example.com/some-dummy-url.js", + type: "script", + }; + + await assertDNRTestMatchOutcome( + { + extension, + testRequest: testRequestMainFrame, + expected: { + matchedRules: [{ ruleId: 1, rulesetId: "ruleset_1" }], + }, + }, + "Expect ruleset_1 to be matched on the main-frame test request" + ); + await assertDNRTestMatchOutcome( + { + extension, + testRequest: testRequestScript, + expected: { + matchedRules: [{ ruleId: 1, rulesetId: "ruleset_2" }], + }, + }, + "Expect ruleset_2 to be matched on the script test request" + ); + + info("Verify DNRStore data persisted on disk for the test extension"); + // The data will not be stored on disk until something is being changed + // from what was already available in the manifest and so in this + // test we save manually (a test for the updateEnabledRulesets will + // take care of asserting that the data has been stored automatically + // on disk when it is meant to). + await dnrStore.save(extension.extension); + + const { storeFile } = dnrStore.getFilePaths(extUUID); + + ok(await IOUtils.exists(storeFile), `DNR storeFile ${storeFile} found`); + + // force deleting the data stored in memory to confirm if it being loaded again from + // the files stored on disk. + dnrStore._data.delete(extUUID); + dnrStore._dataPromises.delete(extUUID); + + info("Verify the expected DNRStore data persisted on disk is loaded back"); + const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" + ); + const addon = await AddonManager.getAddonByID(extension.id); + await addon.disable(); + + ok( + !dnrStore._dataPromises.has(extUUID), + "DNR store read data promise cleared after the extension has been disabled" + ); + ok( + !dnrStore._data.has(extUUID), + "DNR store data cleared from memory after the extension has been disabled" + ); + + await addon.enable(); + await extension.awaitMessage("bgpage:ready"); + await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_2"]); + + await assertDNRStoreData(dnrStore, extension, { + ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), + ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), + }); + + info("Verify matched rules using testMatchOutcome"); + await assertDNRTestMatchOutcome( + { + extension, + testRequest: testRequestMainFrame, + expected: { + matchedRules: [{ ruleId: 1, rulesetId: "ruleset_1" }], + }, + }, + "Expect ruleset_1 to be matched on the main-frame test request" + ); + + info("Verify enabled static rules updated on addon updates"); + await extension.upgrade( + getDNRExtension({ + version: "2.0", + rule_resources: [ + { + id: "ruleset_1", + enabled: false, + path: "ruleset_1.json", + }, + { + id: "ruleset_2", + enabled: true, + path: "ruleset_2.json", + }, + ], + files: { + "ruleset_2.json": JSON.stringify(ruleset2Data), + }, + }) + ); + await extension.awaitMessage("bgpage:ready"); + await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]); + await assertDNRStoreData(dnrStore, extension, { + ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), + }); + + info("Verify matched rules using testMatchOutcome"); + await assertDNRTestMatchOutcome( + { + extension, + testRequest: testRequestMainFrame, + expected: { + matchedRules: [{ ruleId: 1, rulesetId: "ruleset_2" }], + }, + }, + "Expect ruleset_2 to be matched on the main-frame test request" + ); + + info( + "Verify enabled static rules updated on addon updates even if version in the manifest did not change" + ); + await extension.upgrade( + getDNRExtension({ + rule_resources: [ + { + id: "ruleset_1", + enabled: true, + path: "ruleset_1.json", + }, + { + id: "ruleset_2", + enabled: false, + path: "ruleset_2.json", + }, + ], + files: { + "ruleset_1.json": JSON.stringify(ruleset1Data), + }, + }) + ); + await extension.awaitMessage("bgpage:ready"); + await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); + await assertDNRStoreData(dnrStore, extension, { + ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), + }); + + info("Verify matched rules using testMatchOutcome"); + await assertDNRTestMatchOutcome( + { + extension, + testRequest: testRequestMainFrame, + expected: { + matchedRules: [{ ruleId: 1, rulesetId: "ruleset_1" }], + }, + }, + "Expect ruleset_2 to be matched on the main-script test request" + ); + + info( + "Verify updated addon version with no static rules but declarativeNetRequest permission granted" + ); + await extension.upgrade( + getDNRExtension({ + version: "3.0", + rule_resources: undefined, + files: {}, + }) + ); + await extension.awaitMessage("bgpage:ready"); + await assertDNRGetEnabledRulesets(extension, []); + await assertDNRStoreData(dnrStore, extension, {}); + + info("Verify matched rules using testMatchOutcome"); + await assertDNRTestMatchOutcome( + { + extension, + testRequest: testRequestScript, + expected: { + matchedRules: [], + }, + }, + "Expect no match on the script test request on test extension without no static rules" + ); + + info("Verify store file removed on addon uninstall"); + await extension.unload(); + + ok( + !dnrStore._dataPromises.has(extUUID), + "DNR store read data promise cleared after the extension has been unloaded" + ); + ok( + !dnrStore._data.has(extUUID), + "DNR store data cleared from memory after the extension has been unloaded" + ); + + ok( + !(await IOUtils.exists(storeFile)), + `DNR storeFile ${storeFile} removed on addon uninstalled` + ); +}); + +add_task(async function test_load_from_corrupted_data() { + const ruleset1Data = [ + getDNRRule({ + action: { type: "allow" }, + condition: { resourceTypes: ["main_frame"] }, + }), + ]; + + const rule_resources = [ + { + id: "ruleset_1", + enabled: true, + path: "ruleset_1.json", + }, + ]; + + const files = { + "ruleset_1.json": JSON.stringify(ruleset1Data), + }; + + const extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ rule_resources, files }) + ); + + await extension.startup(); + + const extUUID = extension.uuid; + + await extension.awaitMessage("bgpage:ready"); + + const dnrStore = ExtensionDNRStore._getStoreForTesting(); + + info("Verify DNRStore data for the test extension"); + await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); + + await assertDNRStoreData(dnrStore, extension, { + ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), + }); + + info("Verify DNRStore data after loading corrupted store data"); + await dnrStore.save(extension.extension); + + const { storeFile } = dnrStore.getFilePaths(extUUID); + ok(await IOUtils.exists(storeFile), `DNR storeFile ${storeFile} found`); + + const nonCorruptedData = await IOUtils.readJSON(storeFile, { + decompress: true, + }); + + async function testLoadedRulesAfterDataCorruption({ + name, + asyncWriteStoreFile, + expectedCorruptFile, + }) { + info(`Tempering DNR store data: ${name}`); + + await extension.addon.disable(); + + ok( + !dnrStore._dataPromises.has(extUUID), + "DNR store read data promise cleared after the extension has been disabled" + ); + ok( + !dnrStore._data.has(extUUID), + "DNR store data cleared from memory after the extension has been disabled" + ); + + // Make sure we remove a previous corrupt file in case there is one from a previous run. + await IOUtils.remove(expectedCorruptFile, { ignoreAbsent: true }); + + await asyncWriteStoreFile(); + + await extension.addon.enable(); + await extension.awaitMessage("bgpage:ready"); + + info("Verify DNRStore data for the test extension"); + await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); + + await assertDNRStoreData(dnrStore, extension, { + ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), + }); + + await TestUtils.waitForCondition( + () => IOUtils.exists(`${expectedCorruptFile}`), + `Wait for the "${expectedCorruptFile}" file to have been created` + ); + } + + await testLoadedRulesAfterDataCorruption({ + name: "invalid lz4 header", + asyncWriteStoreFile: () => + IOUtils.writeUTF8(storeFile, "not an lz4 compressed file", { + compress: false, + }), + expectedCorruptFile: `${storeFile}.corrupt`, + }); + + await testLoadedRulesAfterDataCorruption({ + name: "invalid json data", + asyncWriteStoreFile: () => + IOUtils.writeUTF8(storeFile, "invalid json data", { compress: true }), + expectedCorruptFile: `${storeFile}-1.corrupt`, + }); + + await testLoadedRulesAfterDataCorruption({ + name: "empty json data", + asyncWriteStoreFile: () => + IOUtils.writeUTF8(storeFile, "{}", { compress: true }), + expectedCorruptFile: `${storeFile}-2.corrupt`, + }); + + await testLoadedRulesAfterDataCorruption({ + name: "invalid staticRulesets property type", + asyncWriteStoreFile: () => + IOUtils.writeUTF8( + storeFile, + JSON.stringify({ + schemaVersion: nonCorruptedData.schemaVersion, + extVersion: extension.extension.version, + staticRulesets: "Not an array", + }), + { compress: true } + ), + expectedCorruptFile: `${storeFile}-3.corrupt`, + }); + + await extension.unload(); +}); + +add_task(async function test_ruleset_validation() { + const invalidRulesetIdCases = [ + { + description: "empty ruleset id", + rule_resources: [ + { + // Invalid empty ruleset id. + id: "", + path: "ruleset_0.json", + enabled: true, + }, + ], + expected: [ + // Validation error emitted from the manifest schema validation. + { + message: /rule_resources\.0\.id: String "" must match/, + }, + ], + }, + { + description: "invalid ruleset id starting with '_'", + rule_resources: [ + { + // Invalid empty ruleset id. + id: "_invalid_ruleset_id", + path: "ruleset_0.json", + enabled: true, + }, + ], + expected: [ + // Validation error emitted from the manifest schema validation. + { + message: + /rule_resources\.0\.id: String "_invalid_ruleset_id" must match/, + }, + ], + }, + { + description: "duplicated ruleset ids", + rule_resources: [ + { + id: "ruleset_2", + path: "ruleset_2.json", + enabled: true, + }, + { + // Duplicated ruleset id. + id: "ruleset_2", + path: "duplicated_ruleset_2.json", + enabled: true, + }, + { + id: "ruleset_3", + path: "ruleset_3.json", + enabled: true, + }, + { + // Other duplicated ruleset id. + id: "ruleset_3", + path: "duplicated_ruleset_3.json", + enabled: true, + }, + ], + // NOTE: this is currently a warning logged from onManifestEntry, and so it would actually + // fail in test harness due to the manifest warning, because it is too late at that point + // the addon is technically already starting at that point. + expectInstallFailed: false, + expected: [ + { + message: + /declarative_net_request: Static ruleset ids should be unique.*: "ruleset_2" at index 1, "ruleset_3" at index 3/, + }, + ], + }, + { + description: "missing mandatory path", + rule_resources: [ + { + // Missing mandatory path. + id: "ruleset_3", + enabled: true, + }, + ], + expected: [ + { + message: /rule_resources\.0: Property "path" is required/, + }, + ], + }, + { + description: "missing mandatory id", + rule_resources: [ + { + // Missing mandatory id. + enabled: true, + path: "missing_ruleset_id.json", + }, + ], + expected: [ + { + message: /rule_resources\.0: Property "id" is required/, + }, + ], + }, + { + description: "duplicated ruleset path", + rule_resources: [ + { + id: "ruleset_2", + path: "ruleset_2.json", + enabled: true, + }, + { + // Duplicate path. + id: "ruleset_3", + path: "ruleset_2.json", + enabled: true, + }, + ], + // NOTE: we couldn't get on agreement about making this a manifest validation error, apparently Chrome doesn't validate it and doesn't + // even report any warning, and so it is logged only as an informative warning but without triggering an install failure. + expectInstallFailed: false, + expected: [ + { + message: + /declarative_net_request: Static rulesets paths are not unique.*: ".*ruleset_2.json" at index 1/, + }, + ], + }, + { + description: "missing mandatory enabled", + rule_resources: [ + { + id: "ruleset_without_enabled", + path: "ruleset.json", + }, + ], + expected: [ + { + message: /rule_resources\.0: Property "enabled" is required/, + }, + ], + }, + { + description: "allows and warns additional properties", + declarative_net_request: { + unexpected_prop: true, + rule_resources: [ + { + id: "ruleset1", + path: "ruleset1.json", + enabled: false, + unexpected_prop: true, + }, + ], + }, + expectInstallFailed: false, + expected: [ + { + message: + /declarative_net_request.unexpected_prop: An unexpected property was found/, + }, + { + message: + /rule_resources.0.unexpected_prop: An unexpected property was found/, + }, + ], + }, + { + description: "invalid ruleset JSON - unexpected comments", + rule_resources: [ + { + id: "invalid_ruleset_with_comments", + path: "invalid_ruleset_with_comments.json", + enabled: true, + }, + ], + files: { + "invalid_ruleset_with_comments.json": + "/* an unexpected inline comment */\n[]", + }, + expectInstallFailed: false, + expected: [ + { + message: + /Reading declarative_net_request .*invalid_ruleset_with_comments\.json: JSON.parse: unexpected character/, + }, + ], + }, + { + description: "invalid ruleset JSON - empty string", + rule_resources: [ + { + id: "invalid_ruleset_emptystring", + path: "invalid_ruleset_emptystring.json", + enabled: true, + }, + ], + files: { + "invalid_ruleset_emptystring.json": JSON.stringify(""), + }, + expectInstallFailed: false, + expected: [ + { + message: + /Reading declarative_net_request .*invalid_ruleset_emptystring\.json: rules file must contain an Array/, + }, + ], + }, + { + description: "invalid ruleset JSON - object", + rule_resources: [ + { + id: "invalid_ruleset_object", + path: "invalid_ruleset_object.json", + enabled: true, + }, + ], + files: { + "invalid_ruleset_object.json": JSON.stringify({}), + }, + expectInstallFailed: false, + expected: [ + { + message: + /Reading declarative_net_request .*invalid_ruleset_object\.json: rules file must contain an Array/, + }, + ], + }, + { + description: "invalid ruleset JSON - null", + rule_resources: [ + { + id: "invalid_ruleset_null", + path: "invalid_ruleset_null.json", + enabled: true, + }, + ], + files: { + "invalid_ruleset_null.json": JSON.stringify(null), + }, + expectInstallFailed: false, + expected: [ + { + message: + /Reading declarative_net_request .*invalid_ruleset_null\.json: rules file must contain an Array/, + }, + ], + }, + ]; + + for (const { + description, + declarative_net_request, + rule_resources, + files, + expected, + expectInstallFailed = true, + } of invalidRulesetIdCases) { + info(`Test manifest validation: ${description}`); + let extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ rule_resources, declarative_net_request, files }) + ); + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + ExtensionTestUtils.failOnSchemaWarnings(false); + if (expectInstallFailed) { + await Assert.rejects( + extension.startup(), + /Install failed/, + "Expected install to fail" + ); + } else { + await extension.startup(); + await extension.awaitMessage("bgpage:ready"); + await extension.unload(); + } + ExtensionTestUtils.failOnSchemaWarnings(true); + }); + + AddonTestUtils.checkMessages(messages, { expected }); + } +}); + +add_task(async function test_updateEnabledRuleset_id_validation() { + const rule_resources = [ + { + id: "ruleset_1", + enabled: true, + path: "ruleset_1.json", + }, + { + id: "ruleset_2", + enabled: false, + path: "ruleset_2.json", + }, + ]; + + const ruleset1Data = [ + getDNRRule({ + action: { type: "allow" }, + condition: { resourceTypes: ["main_frame"] }, + }), + ]; + const ruleset2Data = [ + getDNRRule({ + action: { type: "block" }, + condition: { resourceTypes: ["main_frame", "script"] }, + }), + ]; + + const files = { + "ruleset_1.json": JSON.stringify(ruleset1Data), + "ruleset_2.json": JSON.stringify(ruleset2Data), + }; + + let extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ rule_resources, files }) + ); + + await extension.startup(); + await extension.awaitMessage("bgpage:ready"); + + await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); + + const dnrStore = ExtensionDNRStore._getStoreForTesting(); + await assertDNRStoreData(dnrStore, extension, { + ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), + }); + + const invalidStaticRulesetIds = [ + // The following two are reserved for session and dynamic rules. + "_session", + "_dynamic", + "ruleset_non_existing", + ]; + + for (const invalidRSId of invalidStaticRulesetIds) { + extension.sendMessage( + "updateEnabledRulesets", + // Only in rulesets to be disabled. + { disableRulesetIds: [invalidRSId] }, + // Only in rulesets to be enabled. + { enableRulesetIds: [invalidRSId] }, + // In both rulesets to be enabled and disabled. + { disableRulesetIds: [invalidRSId], enableRulesetIds: [invalidRSId] }, + // Along with existing rulesets (and expected the existing rulesets + // to stay unchanged due to the invalid ruleset ids.) + { + disableRulesetIds: [invalidRSId, "ruleset_1"], + enableRulesetIds: [invalidRSId, "ruleset_2"], + } + ); + const [ + resInDisable, + resInEnable, + resInEnableAndDisable, + resInSameRequestAsValid, + ] = await extension.awaitMessage("updateEnabledRulesets:done"); + await Assert.rejects( + Promise.reject(resInDisable?.rejectedWithErrorMessage), + new RegExp(`Invalid ruleset id: "${invalidRSId}"`), + `Got the expected rejection on invalid ruleset id "${invalidRSId}" in disableRulesetIds` + ); + await Assert.rejects( + Promise.reject(resInEnable?.rejectedWithErrorMessage), + new RegExp(`Invalid ruleset id: "${invalidRSId}"`), + `Got the expected rejection on invalid ruleset id "${invalidRSId}" in enableRulesetIds` + ); + await Assert.rejects( + Promise.reject(resInEnableAndDisable?.rejectedWithErrorMessage), + new RegExp(`Invalid ruleset id: "${invalidRSId}"`), + `Got the expected rejection on invalid ruleset id "${invalidRSId}" in both enable/disableRulesetIds` + ); + await Assert.rejects( + Promise.reject(resInSameRequestAsValid?.rejectedWithErrorMessage), + new RegExp(`Invalid ruleset id: "${invalidRSId}"`), + `Got the expected rejection on invalid ruleset id "${invalidRSId}" along with valid ruleset ids` + ); + } + + // Confirm that the expected rulesets didn't change neither. + await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); + await assertDNRStoreData(dnrStore, extension, { + ruleset_1: getSchemaNormalizedRules(extension, ruleset1Data), + }); + + // - List the same ruleset ids more than ones is expected to work and + // to be resulting in the same set of rules being enabled + // - Disabling and Enabling the same ruleset id should result in the + // ruleset being enabled. + await extension.sendMessage("updateEnabledRulesets", { + disableRulesetIds: [ + "ruleset_1", + "ruleset_1", + "ruleset_2", + "ruleset_2", + "ruleset_2", + ], + enableRulesetIds: ["ruleset_2", "ruleset_2"], + }); + Assert.deepEqual( + await extension.awaitMessage("updateEnabledRulesets:done"), + [undefined], + "Expect the updateEnabledRulesets to result successfully" + ); + + await assertDNRGetEnabledRulesets(extension, ["ruleset_2"]); + await assertDNRStoreData(dnrStore, extension, { + ruleset_2: getSchemaNormalizedRules(extension, ruleset2Data), + }); + + await extension.unload(); +}); + +add_task(async function test_getAvailableStaticRulesCountAndLimits() { + // NOTE: this test is going to load and validate the maximum amount of static rules + // that an extension can enable, which on slower builds (in particular in tsan builds, + // e.g. see Bug 1803801) have a higher chance that the test extension may have hit the + // idle timeout and being suspended by the time the test is going to trigger API method + // calls through test API events (which do not expect the lifetime of the event page). + Services.prefs.setBoolPref("extensions.background.idle.enabled", false); + + const dnrStore = ExtensionDNRStore._getStoreForTesting(); + const { GUARANTEED_MINIMUM_STATIC_RULES } = ExtensionDNRLimits; + equal( + typeof GUARANTEED_MINIMUM_STATIC_RULES, + "number", + "Expect GUARANTEED_MINIMUM_STATIC_RULES to be a number" + ); + + const availableStaticRulesCount = GUARANTEED_MINIMUM_STATIC_RULES; + + const rule_resources = [ + { + id: "ruleset_0", + path: "/ruleset_0.json", + enabled: true, + }, + { + id: "ruleset_1", + path: "/ruleset_1.json", + enabled: true, + }, + // A ruleset initially disabled (to make sure it doesn't count for the + // rules count limit). + { + id: "ruleset_disabled", + path: "/ruleset_disabled.json", + enabled: false, + }, + // A ruleset including an invalid rule and valid rule. + { + id: "ruleset_withInvalid", + path: "/ruleset_withInvalid.json", + enabled: false, + }, + // An empty ruleset (to make sure it can still be enabled/disabled just fine, + // e.g. in case on some browser version all rules are technically invalid). + { + id: "ruleset_empty", + path: "/ruleset_empty.json", + enabled: false, + }, + ]; + + const files = {}; + const rules = {}; + + const rulesetDisabledData = [getDNRRule({ id: 1 })]; + const ruleValid = getDNRRule({ id: 2, action: { type: "allow" } }); + const rulesetWithInvalidData = [ + getDNRRule({ id: 1, action: { type: "invalid_action" } }), + ruleValid, + ]; + + rules.ruleset_0 = [getDNRRule({ id: 1 }), getDNRRule({ id: 2 })]; + + rules.ruleset_1 = []; + for (let i = 0; i < availableStaticRulesCount; i++) { + rules.ruleset_1.push(getDNRRule({ id: i + 1 })); + } + + for (const [k, v] of Object.entries(rules)) { + files[`${k}.json`] = JSON.stringify(v); + } + files[`ruleset_disabled.json`] = JSON.stringify(rulesetDisabledData); + files[`ruleset_withInvalid.json`] = JSON.stringify(rulesetWithInvalidData); + files[`ruleset_empty.json`] = JSON.stringify([]); + + const extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ + id: "dnr-getAvailable-count-@mochitest", + rule_resources, + files, + }) + ); + + await extension.startup(); + await extension.awaitMessage("bgpage:ready"); + + async function updateEnabledRulesets({ expectedErrorMessage, ...options }) { + // Note: options = { disableRulesetIds, enableRulesetIds } + extension.sendMessage("updateEnabledRulesets", options); + let [result] = await extension.awaitMessage("updateEnabledRulesets:done"); + if (expectedErrorMessage) { + Assert.deepEqual( + result, + { rejectedWithErrorMessage: expectedErrorMessage }, + "updateEnabledRulesets() should reject with the given error" + ); + } else { + Assert.deepEqual( + result, + undefined, + "updateEnabledRulesets() should resolve without error" + ); + } + } + + const expectedEnabledRulesets = {}; + expectedEnabledRulesets.ruleset_0 = getSchemaNormalizedRules( + extension, + rules.ruleset_0 + ); + + info( + "Expect ruleset_1 to not be enabled because along with ruleset_0 exceeded the static rules count limit" + ); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); + + await assertDNRGetAvailableStaticRuleCount( + extension, + availableStaticRulesCount - rules.ruleset_0.length, + "Got the available static rule count on ruleset_0 initially enabled" + ); + + // Try to enable ruleset_1 again from the API method. + await updateEnabledRulesets({ + enableRulesetIds: ["ruleset_1"], + expectedErrorMessage: `Number of rules across all enabled static rulesets exceeds GUARANTEED_MINIMUM_STATIC_RULES if ruleset "ruleset_1" were to be enabled.`, + }); + + info( + "Expect ruleset_1 to not be enabled because still exceeded the static rules count limit" + ); + await assertDNRGetEnabledRulesets(extension, ["ruleset_0"]); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); + + await assertDNRGetAvailableStaticRuleCount( + extension, + availableStaticRulesCount - rules.ruleset_0.length, + "Got the available static rule count on ruleset_0 still the only one enabled" + ); + + await updateEnabledRulesets({ + disableRulesetIds: ["ruleset_0"], + enableRulesetIds: ["ruleset_1"], + }); + + info("Expect ruleset_1 to be enabled along with disabling ruleset_0"); + await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); + delete expectedEnabledRulesets.ruleset_0; + expectedEnabledRulesets.ruleset_1 = getSchemaNormalizedRules( + extension, + rules.ruleset_1 + ); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets, { + // Assert total amount of expected rules and only the first and last rule + // individually, to avoid generating a huge amount of logs and potential + // timeout failures on slower builds. + assertIndividualRules: false, + }); + + await assertDNRGetAvailableStaticRuleCount( + extension, + 0, + "Expect no additional static rules count available when ruleset_1 is enabled" + ); + + info( + "Expect ruleset_disabled to stay disabled because along with ruleset_1 exceeeds the limits" + ); + await updateEnabledRulesets({ + enableRulesetIds: ["ruleset_disabled"], + expectedErrorMessage: `Number of rules across all enabled static rulesets exceeds GUARANTEED_MINIMUM_STATIC_RULES if ruleset "ruleset_disabled" were to be enabled.`, + }); + await assertDNRGetEnabledRulesets(extension, ["ruleset_1"]); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets, { + // Assert total amount of expected rules and only the first and last rule + // individually, to avoid generating a huge amount of logs and potential + // timeout failures on slower builds. + assertIndividualRules: false, + }); + await assertDNRGetAvailableStaticRuleCount( + extension, + 0, + "Expect no additional static rules count available" + ); + + info("Expect ruleset_empty to be enabled despite having reached the limit"); + await updateEnabledRulesets({ + enableRulesetIds: ["ruleset_empty"], + }); + await assertDNRGetEnabledRulesets(extension, ["ruleset_1", "ruleset_empty"]); + await assertDNRStoreData( + dnrStore, + extension, + { + ...expectedEnabledRulesets, + ruleset_empty: [], + }, + // Assert total amount of expected rules and only the first and last rule + // individually, to avoid generating a huge amount of logs and potential + // timeout failures on slower builds. + { assertIndividualRules: false } + ); + await assertDNRGetAvailableStaticRuleCount( + extension, + 0, + "Expect no additional static rules count available" + ); + + info("Expect invalid rules to not be counted towards the limits"); + await updateEnabledRulesets({ + disableRulesetIds: ["ruleset_1", "ruleset_empty"], + enableRulesetIds: ["ruleset_withInvalid"], + }); + await assertDNRGetEnabledRulesets(extension, ["ruleset_withInvalid"]); + await assertDNRStoreData(dnrStore, extension, { + // Only the valid rule has been actually loaded, and the invalid one + // ignored. + ruleset_withInvalid: [ruleValid], + }); + await assertDNRGetAvailableStaticRuleCount( + extension, + availableStaticRulesCount - 1, + "Expect only valid rules to be counted" + ); + + await extension.unload(); + + Services.prefs.clearUserPref("extensions.background.idle.enabled"); +}); + +add_task(async function test_static_rulesets_limits() { + const dnrStore = ExtensionDNRStore._getStoreForTesting(); + + const getRulesetManifestData = (rulesetNumber, enabled) => { + return { + id: `ruleset_${rulesetNumber}`, + enabled, + path: `ruleset_${rulesetNumber}.json`, + }; + }; + const { + MAX_NUMBER_OF_STATIC_RULESETS, + MAX_NUMBER_OF_ENABLED_STATIC_RULESETS, + } = ExtensionDNRLimits; + + equal( + typeof MAX_NUMBER_OF_STATIC_RULESETS, + "number", + "Expect MAX_NUMBER_OF_STATIC_RULESETS to be a number" + ); + equal( + typeof MAX_NUMBER_OF_ENABLED_STATIC_RULESETS, + "number", + "Expect MAX_NUMBER_OF_ENABLED_STATIC_RULESETS to be a number" + ); + Assert.greater( + MAX_NUMBER_OF_STATIC_RULESETS, + MAX_NUMBER_OF_ENABLED_STATIC_RULESETS, + "Expect MAX_NUMBER_OF_STATIC_RULESETS to be greater" + ); + + const rules = [getDNRRule()]; + + const rule_resources = []; + const files = {}; + for (let i = 0; i < MAX_NUMBER_OF_STATIC_RULESETS + 1; i++) { + const enabled = i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS + 1; + files[`ruleset_${i}.json`] = JSON.stringify(rules); + rule_resources.push(getRulesetManifestData(i, enabled)); + } + + let extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ + rule_resources, + files, + }) + ); + + const expectedEnabledRulesets = {}; + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + await extension.awaitMessage("bgpage:ready"); + + for (let i = 0; i < MAX_NUMBER_OF_ENABLED_STATIC_RULESETS; i++) { + expectedEnabledRulesets[`ruleset_${i}`] = getSchemaNormalizedRules( + extension, + rules + ); + } + + await assertDNRGetEnabledRulesets( + extension, + Array.from(Object.keys(expectedEnabledRulesets)) + ); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); + }); + + AddonTestUtils.checkMessages(messages, { + expected: [ + // Warnings emitted from the manifest schema validation. + { + message: + /declarative_net_request: Static rulesets are exceeding the MAX_NUMBER_OF_STATIC_RULESETS limit/, + }, + { + message: + /declarative_net_request: Enabled static rulesets are exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS limit .* "ruleset_10"/, + }, + // Error reported on the browser console as part of loading enabled rulesets) + // on enabled rulesets being ignored because exceeding the limit. + { + message: + /Ignoring enabled static ruleset exceeding the MAX_NUMBER_OF_ENABLED_STATIC_RULESETS .* "ruleset_10"/, + }, + ], + }); + + info( + "Verify updateEnabledRulesets reject when the request is exceeding the enabled rulesets count limit" + ); + extension.sendMessage("updateEnabledRulesets", { + disableRulesetIds: ["ruleset_0"], + enableRulesetIds: ["ruleset_10", "ruleset_11"], + }); + + await Assert.rejects( + extension.awaitMessage("updateEnabledRulesets:done").then(results => { + if (results[0].rejectedWithErrorMessage) { + return Promise.reject(new Error(results[0].rejectedWithErrorMessage)); + } + return results[0]; + }), + /updatedEnabledRulesets request is exceeding MAX_NUMBER_OF_ENABLED_STATIC_RULESETS/, + "Expected rejection on updateEnabledRulesets exceeting enabled rulesets count limit" + ); + + // Confirm that the expected rulesets didn't change neither. + await assertDNRGetEnabledRulesets( + extension, + Array.from(Object.keys(expectedEnabledRulesets)) + ); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); + + info( + "Verify updateEnabledRulesets applies the expected changes when resolves successfully" + ); + extension.sendMessage( + "updateEnabledRulesets", + { + disableRulesetIds: ["ruleset_0"], + enableRulesetIds: ["ruleset_10"], + }, + { + disableRulesetIds: ["ruleset_10"], + enableRulesetIds: ["ruleset_11"], + } + ); + await extension.awaitMessage("updateEnabledRulesets:done"); + + // Expect ruleset_0 disabled, ruleset_10 to be enabled but then disabled by the + // second update queued after the first one, and ruleset_11 to be enabled. + delete expectedEnabledRulesets.ruleset_0; + expectedEnabledRulesets.ruleset_11 = getSchemaNormalizedRules( + extension, + rules + ); + + await assertDNRGetEnabledRulesets( + extension, + Array.from(Object.keys(expectedEnabledRulesets)) + ); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); + + // Ensure all changes were stored and reloaded from disk store and the + // DNR store update queue can accept new updates. + info("Verify static rules load and updates after extension is restarted"); + + // NOTE: promiseRestartManager will not be enough to make sure the + // DNR store data for the test extension is going to be loaded from + // the DNR startup cache file. + // See test_ext_dnr_startup_cache.js for a test case that more completely + // simulates ExtensionDNRStore initialization on browser restart. + await AddonTestUtils.promiseRestartManager(); + + await extension.awaitStartup(); + await extension.awaitMessage("bgpage:ready"); + await assertDNRGetEnabledRulesets( + extension, + Array.from(Object.keys(expectedEnabledRulesets)) + ); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); + + extension.sendMessage("updateEnabledRulesets", { + disableRulesetIds: ["ruleset_11"], + }); + await extension.awaitMessage("updateEnabledRulesets:done"); + delete expectedEnabledRulesets.ruleset_11; + await assertDNRGetEnabledRulesets( + extension, + Array.from(Object.keys(expectedEnabledRulesets)) + ); + await assertDNRStoreData(dnrStore, extension, expectedEnabledRulesets); + + await extension.unload(); +}); + +add_task(async function test_tabId_conditions_invalid_in_static_rules() { + const ruleset1_with_tabId_condition = [ + getDNRRule({ id: 1, condition: { tabIds: [1] } }), + getDNRRule({ id: 3, condition: { urlFilter: "valid-ruleset1-rule" } }), + ]; + + const ruleset2_with_excludeTabId_condition = [ + getDNRRule({ id: 2, condition: { excludedTabIds: [1] } }), + getDNRRule({ id: 3, condition: { urlFilter: "valid-ruleset2-rule" } }), + ]; + + const rule_resources = [ + { + id: "ruleset1_with_tabId_condition", + enabled: true, + path: "ruleset1.json", + }, + { + id: "ruleset2_with_excludeTabId_condition", + enabled: true, + path: "ruleset2.json", + }, + ]; + + const files = { + "ruleset1.json": JSON.stringify(ruleset1_with_tabId_condition), + "ruleset2.json": JSON.stringify(ruleset2_with_excludeTabId_condition), + }; + + const extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ + id: "tabId-invalid-in-session-rules@mochitest", + rule_resources, + files, + }) + ); + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + ExtensionTestUtils.failOnSchemaWarnings(false); + await extension.startup(); + ExtensionTestUtils.failOnSchemaWarnings(true); + await extension.awaitMessage("bgpage:ready"); + await assertDNRGetEnabledRulesets(extension, [ + "ruleset1_with_tabId_condition", + "ruleset2_with_excludeTabId_condition", + ]); + }); + + AddonTestUtils.checkMessages(messages, { + expected: [ + { + message: + /"ruleset1_with_tabId_condition": tabIds and excludedTabIds can only be specified in session rules/, + }, + { + message: + /"ruleset2_with_excludeTabId_condition": tabIds and excludedTabIds can only be specified in session rules/, + }, + ], + }); + + info("Expect the invalid rule to not be enabled"); + const dnrStore = ExtensionDNRStore._getStoreForTesting(); + // Expect the two valid rules to have been loaded as expected. + await assertDNRStoreData(dnrStore, extension, { + ruleset1_with_tabId_condition: getSchemaNormalizedRules(extension, [ + ruleset1_with_tabId_condition[1], + ]), + ruleset2_with_excludeTabId_condition: getSchemaNormalizedRules(extension, [ + ruleset2_with_excludeTabId_condition[1], + ]), + }); + + await extension.unload(); +}); + +add_task(async function test_dnr_all_rules_disabled_allowed() { + const ruleset1 = [ + getDNRRule({ id: 3, condition: { urlFilter: "valid-ruleset1-rule" } }), + ]; + + const rule_resources = [ + { + id: "ruleset1", + enabled: true, + path: "ruleset1.json", + }, + ]; + + const files = { + "ruleset1.json": JSON.stringify(ruleset1), + }; + + const extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ + id: "all-static-rulesets-disabled-allowed@mochitest", + rule_resources, + files, + }) + ); + + await extension.startup(); + await extension.awaitMessage("bgpage:ready"); + + await assertDNRGetEnabledRulesets(extension, ["ruleset1"]); + + const dnrStore = ExtensionDNRStore._getStoreForTesting(); + await assertDNRStoreData(dnrStore, extension, { + ruleset1: getSchemaNormalizedRules(extension, ruleset1), + }); + + info("Disable static ruleset1"); + extension.sendMessage("updateEnabledRulesets", { + disableRulesetIds: ["ruleset1"], + }); + await extension.awaitMessage("updateEnabledRulesets:done"); + await assertDNRGetEnabledRulesets(extension, []); + await assertDNRStoreData(dnrStore, extension, {}); + + info("Verify that static ruleset1 is still disable after browser restart"); + + // NOTE: promiseRestartManager will not be enough to make sure the + // DNR store data for the test extension is going to be loaded from + // the DNR startup cache file. + // See test_ext_dnr_startup_cache.js for a test case that more completely + // simulates ExtensionDNRStore initialization on browser restart. + await AddonTestUtils.promiseRestartManager(); + + await extension.awaitStartup; + await ExtensionDNR.ensureInitialized(extension.extension); + await extension.awaitMessage("bgpage:ready"); + + await assertDNRGetEnabledRulesets(extension, []); + await assertDNRStoreData(dnrStore, extension, {}); + + await extension.unload(); +}); + +add_task(async function test_static_rules_telemetry() { + resetTelemetryData(); + + const ruleset1 = [ + getDNRRule({ + id: 1, + action: { type: "block" }, + condition: { + resourceTypes: ["xmlhttprequest"], + requestDomains: ["example.com"], + }, + }), + ]; + const ruleset2 = [ + getDNRRule({ + id: 1, + action: { type: "block" }, + condition: { + resourceTypes: ["xmlhttprequest"], + requestDomains: ["example.org"], + }, + }), + getDNRRule({ + id: 2, + action: { type: "block" }, + condition: { + resourceTypes: ["xmlhttprequest"], + requestDomains: ["example2.org"], + }, + }), + ]; + + const rule_resources = [ + { + id: "ruleset1", + enabled: false, + path: "ruleset1.json", + }, + { + id: "ruleset2", + enabled: false, + path: "ruleset2.json", + }, + ]; + + const files = { + "ruleset1.json": JSON.stringify(ruleset1), + "ruleset2.json": JSON.stringify(ruleset2), + }; + + const extension = ExtensionTestUtils.loadExtension( + getDNRExtension({ + id: "tabId-invalid-in-session-rules@mochitest", + rule_resources, + files, + }) + ); + + assertDNRTelemetryMetricsNoSamples( + [ + { + metric: "validateRulesTime", + mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS", + mirroredType: "histogram", + }, + { + metric: "evaluateRulesTime", + mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS", + mirroredType: "histogram", + }, + ], + "before test extension have been loaded" + ); + + await extension.startup(); + await extension.awaitMessage("bgpage:ready"); + + await assertDNRGetEnabledRulesets(extension, []); + + assertDNRTelemetryMetricsNoSamples( + [ + { + metric: "validateRulesTime", + mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS", + mirroredType: "histogram", + }, + ], + "after test extension loaded with all static rulesets disabled" + ); + + info("Enable static ruleset1"); + extension.sendMessage("updateEnabledRulesets", { + enableRulesetIds: ["ruleset1"], + }); + await extension.awaitMessage("updateEnabledRulesets:done"); + + await assertDNRGetEnabledRulesets(extension, ["ruleset1"]); + + // Expect one sample after enabling ruleset1. + let expectedValidateRulesTimeSamples = 1; + assertDNRTelemetryMetricsSamplesCount( + [ + { + metric: "validateRulesTime", + mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS", + mirroredType: "histogram", + expectedSamplesCount: expectedValidateRulesTimeSamples, + }, + ], + "after enabling static rulesets1" + ); + + info("Enable static ruleset2"); + extension.sendMessage("updateEnabledRulesets", { + enableRulesetIds: ["ruleset2"], + }); + await extension.awaitMessage("updateEnabledRulesets:done"); + + await assertDNRGetEnabledRulesets(extension, ["ruleset1", "ruleset2"]); + + // Expect one new sample after enabling ruleset2. + expectedValidateRulesTimeSamples += 1; + assertDNRTelemetryMetricsSamplesCount( + [ + { + metric: "validateRulesTime", + mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS", + mirroredType: "histogram", + expectedSamplesCount: expectedValidateRulesTimeSamples, + }, + ], + "after enabling static rulesets2" + ); + + await extension.addon.disable(); + + assertDNRTelemetryMetricsSamplesCount( + [ + { + metric: "validateRulesTime", + mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS", + mirroredType: "histogram", + expectedSamplesCount: expectedValidateRulesTimeSamples, + }, + ], + "no new samples expected after disabling test extension" + ); + + await extension.addon.enable(); + await extension.awaitMessage("bgpage:ready"); + await ExtensionDNR.ensureInitialized(extension.extension); + + // Expect 2 new samples after re-enabling the addon with + // the 2 rulesets enabled being loaded from the DNR store file. + expectedValidateRulesTimeSamples += 2; + assertDNRTelemetryMetricsSamplesCount( + [ + { + metric: "validateRulesTime", + mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS", + mirroredType: "histogram", + expectedSamplesCount: expectedValidateRulesTimeSamples, + }, + ], + "after re-enabling test extension" + ); + + info("Disable static ruleset1"); + + extension.sendMessage("updateEnabledRulesets", { + disableRulesetIds: ["ruleset1"], + }); + await extension.awaitMessage("updateEnabledRulesets:done"); + + await assertDNRGetEnabledRulesets(extension, ["ruleset2"]); + + assertDNRTelemetryMetricsSamplesCount( + [ + { + metric: "validateRulesTime", + mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS", + mirroredType: "histogram", + expectedSamplesCount: expectedValidateRulesTimeSamples, + }, + ], + "no new validation should be hit after disabling ruleset1" + ); + + info("Verify telemetry recorded on rules evaluation"); + extension.sendMessage("updateEnabledRulesets", { + enableRulesetIds: ["ruleset1"], + disableRulesetIds: ["ruleset2"], + }); + await extension.awaitMessage("updateEnabledRulesets:done"); + await assertDNRGetEnabledRulesets(extension, ["ruleset1"]); + + assertDNRTelemetryMetricsNoSamples( + [ + { + metric: "evaluateRulesTime", + mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS", + mirroredType: "histogram", + }, + { + metric: "evaluateRulesCountMax", + mirroredName: "extensions.apis.dnr.evaluate_rules_count_max", + mirroredType: "scalar", + }, + ], + "before any request have been intercepted" + ); + + Assert.equal( + await fetch("http://example.com/").then(res => res.text()), + "response from server", + "DNR should not block system requests" + ); + + assertDNRTelemetryMetricsNoSamples( + [ + { + metric: "evaluateRulesTime", + mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS", + mirroredType: "histogram", + }, + { + metric: "evaluateRulesCountMax", + mirroredName: "extensions.apis.dnr.evaluate_rules_count_max", + mirroredType: "scalar", + }, + ], + "after restricted request have been intercepted (but no rules evaluated)" + ); + + const page = await ExtensionTestUtils.loadContentPage("http://example.com"); + const callPageFetch = async () => { + Assert.equal( + await page.spawn([], () => { + return this.content.fetch("http://example.com/").then( + res => res.text(), + err => err.message + ); + }), + "NetworkError when attempting to fetch resource.", + "DNR should have blocked test request to example.com" + ); + }; + + // Expect one sample recorded on evaluating rules for the + // top level navigation. + let expectedEvaluateRulesTimeSamples = 1; + assertDNRTelemetryMetricsSamplesCount( + [ + { + metric: "evaluateRulesTime", + mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS", + mirroredType: "histogram", + expectedSamplesCount: expectedEvaluateRulesTimeSamples, + }, + ], + "evaluateRulesTime should be collected after evaluated rulesets" + ); + // Expect same number of rules included in the single ruleset + // currently enabled. + let expectedEvaluateRulesCountMax = ruleset1.length; + assertDNRTelemetryMetricsGetValueEq( + [ + { + metric: "evaluateRulesCountMax", + mirroredName: "extensions.apis.dnr.evaluate_rules_count_max", + mirroredType: "scalar", + expectedGetValue: expectedEvaluateRulesCountMax, + }, + ], + "evaluateRulesCountMax should be collected after evaluated rulesets1" + ); + + await callPageFetch(); + + // Expect one new sample reported on evaluating rules for the + // first fetch request originated from the test page. + expectedEvaluateRulesTimeSamples += 1; + assertDNRTelemetryMetricsSamplesCount( + [ + { + metric: "evaluateRulesTime", + mirroredName: "WEBEXT_DNR_EVALUATE_RULES_MS", + mirroredType: "histogram", + expectedSamplesCount: expectedEvaluateRulesTimeSamples, + }, + ], + "evaluateRulesTime should be collected after evaluated rulesets" + ); + + extension.sendMessage("updateEnabledRulesets", { + enableRulesetIds: ["ruleset2"], + }); + await extension.awaitMessage("updateEnabledRulesets:done"); + await assertDNRGetEnabledRulesets(extension, ["ruleset1", "ruleset2"]); + + await callPageFetch(); + + // Expect 3 rules with both rulesets enabled + // (1 from ruleset1 and 2 more from ruleset2). + expectedEvaluateRulesCountMax += ruleset2.length; + assertDNRTelemetryMetricsGetValueEq( + [ + { + metric: "evaluateRulesCountMax", + mirroredName: "extensions.apis.dnr.evaluate_rules_count_max", + mirroredType: "scalar", + expectedGetValue: expectedEvaluateRulesCountMax, + }, + ], + "evaluateRulesCountMax should have been increased after enabling ruleset2" + ); + + extension.sendMessage("updateEnabledRulesets", { + disableRulesetIds: ["ruleset2"], + }); + await extension.awaitMessage("updateEnabledRulesets:done"); + await assertDNRGetEnabledRulesets(extension, ["ruleset1"]); + + await callPageFetch(); + + assertDNRTelemetryMetricsGetValueEq( + [ + { + metric: "evaluateRulesCountMax", + mirroredName: "extensions.apis.dnr.evaluate_rules_count_max", + mirroredType: "scalar", + expectedGetValue: expectedEvaluateRulesCountMax, + }, + ], + "evaluateRulesCountMax should have not been decreased after disabling ruleset2" + ); + + await page.close(); + + await extension.unload(); +}); |