summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_dnr_startup_cache.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_dnr_startup_cache.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_dnr_startup_cache.js651
1 files changed, 651 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_dnr_startup_cache.js b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_startup_cache.js
new file mode 100644
index 0000000000..bcb05eec23
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_dnr_startup_cache.js
@@ -0,0 +1,651 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Schemas } = ChromeUtils.importESModule(
+ "resource://gre/modules/Schemas.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs",
+ ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ aomStartup: [
+ "@mozilla.org/addons/addon-manager-startup;1",
+ "amIAddonManagerStartup",
+ ],
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+Services.scriptloader.loadSubScript(
+ Services.io.newFileURI(do_get_file("head_dnr.js")).spec,
+ this
+);
+
+const EXT_ID = "test-dnr-store-startup-cache@test-extension";
+const TEMP_EXT_ID = "test-dnr-store-temporarily-installed@test-extension";
+
+// Test rulesets should include fields that are special cased during Rule object deserialization
+// from the plain objects loaded from the StartupCache data objects.
+// In particular regexFilter are included to make sure that RuleValidator.deserializeRule is
+// internally compiling the regexFilter and storing it into the RuleCondition instance as expected
+// (implicitly asserted internally by the test helper assertDNRStoreData in head_dnr.js).
+const RULESET_1_DATA = [
+ getDNRRule({
+ id: 1,
+ action: { type: "allow" },
+ condition: { resourceTypes: ["main_frame"], regexFilter: "http://from/$" },
+ }),
+ getDNRRule({
+ id: 2,
+ action: { type: "allow" },
+ condition: {
+ resourceTypes: ["main_frame"],
+ regexFilter: "http://from2/$",
+ isUrlFilterCaseSensitive: true,
+ },
+ }),
+];
+const RULESET_2_DATA = [
+ getDNRRule({
+ action: { type: "block" },
+ condition: { resourceTypes: ["main_frame", "script"] },
+ }),
+];
+
+function getDNRExtension({
+ id = EXT_ID,
+ version = "1.0",
+ useAddonManager = "permanent",
+ background,
+ 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,
+ };
+}
+
+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_dnr_startup_cache_save_and_load() {
+ resetTelemetryData();
+
+ const rule_resources = [
+ {
+ id: "ruleset_1",
+ enabled: true,
+ path: "ruleset_1.json",
+ },
+ {
+ id: "ruleset_2",
+ enabled: false,
+ path: "ruleset_2.json",
+ },
+ ];
+ const files = {
+ "ruleset_1.json": JSON.stringify(RULESET_1_DATA),
+ "ruleset_2.json": JSON.stringify(RULESET_2_DATA),
+ };
+
+ let dnrStore = ExtensionDNRStore._getStoreForTesting();
+ let sandboxStoreSpies = sinon.createSandbox();
+ const spyScheduleCacheDataSave = sandboxStoreSpies.spy(
+ dnrStore,
+ "scheduleCacheDataSave"
+ );
+
+ const extension = ExtensionTestUtils.loadExtension(
+ getDNRExtension({ rule_resources, files })
+ );
+
+ const temporarilyInstalledExt = ExtensionTestUtils.loadExtension(
+ getDNRExtension({
+ id: TEMP_EXT_ID,
+ useAddonManager: "temporary",
+ rule_resources,
+ files,
+ })
+ );
+
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ },
+ ],
+ "before any test extensions have been loaded"
+ );
+
+ await temporarilyInstalledExt.startup();
+ await extension.startup();
+ info(
+ "Wait for DNR initialization completed for the temporarily installed extension"
+ );
+ await ExtensionDNR.ensureInitialized(temporarilyInstalledExt.extension);
+ info(
+ "Wait for DNR initialization completed for the permanently installed extension"
+ );
+ await ExtensionDNR.ensureInitialized(extension.extension);
+
+ assertDNRTelemetryMetricsSamplesCount(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ expectedSamplesCount: 2,
+ },
+ ],
+ "after two test extensions have been loaded"
+ );
+
+ Assert.equal(
+ spyScheduleCacheDataSave.callCount,
+ 1,
+ "Expect ExtensionDNRStore scheduleCacheDataSave method to have been called once"
+ );
+
+ sandboxStoreSpies.restore();
+
+ const extUUID = extension.uuid;
+ const { cacheFile } = dnrStore.getFilePaths(extUUID);
+
+ await assertDNRStoreData(dnrStore, extension, {
+ ruleset_1: getSchemaNormalizedRules(extension, RULESET_1_DATA),
+ });
+
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ {
+ metric: "startupCacheWriteTime",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_WRITE_MS",
+ mirroredType: "histogram",
+ },
+ {
+ metric: "startupCacheWriteSize",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_WRITE_BYTES",
+ mirroredType: "histogram",
+ },
+ // Expected no startup cache file to be loaded or used for a newly installed extension.
+ {
+ metric: "startupCacheReadSize",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_READ_BYTES",
+ mirroredType: "histogram",
+ },
+ {
+ metric: "startupCacheReadTime",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_READ_MS",
+ mirroredType: "histogram",
+ },
+ {
+ metric: "startupCacheEntries",
+ label: "miss",
+ mirroredName: "extensions.apis.dnr.startup_cache_entries",
+ mirroredType: "keyedScalar",
+ },
+ {
+ metric: "startupCacheEntries",
+ label: "hit",
+ mirroredName: "extensions.apis.dnr.startup_cache_entries",
+ mirroredType: "keyedScalar",
+ },
+ ],
+ "on loading dnr rules for newly installed extension"
+ );
+ await dnrStore.waitSaveCacheDataForTesting();
+ assertDNRTelemetryMetricsSamplesCount(
+ [
+ {
+ metric: "startupCacheWriteTime",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_WRITE_MS",
+ mirroredType: "histogram",
+ expectedSamplesCount: 1,
+ },
+ {
+ metric: "startupCacheWriteSize",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_WRITE_BYTES",
+ mirroredType: "histogram",
+ expectedSamplesCount: 1,
+ },
+ ],
+ "after writing DNR startup cache data to disk"
+ );
+
+ ok(
+ await IOUtils.exists(cacheFile),
+ "Expect the DNR store startupCache file exist"
+ );
+
+ const assertDNRStoreDataLoadOnStartup = async ({
+ expectLoadedFromCache,
+ expectClearLastUpdateTagPref,
+ }) => {
+ info(
+ `Mock browser restart and assert DNR rules ${
+ expectLoadedFromCache ? "NOT " : ""
+ }going through Schemas.normalize`
+ );
+ await AddonTestUtils.promiseShutdownManager();
+ // Recreate the DNR store to more easily mock its initial state after a browser restart.
+ dnrStore = ExtensionDNRStore._recreateStoreForTesting();
+ const StoreData = ExtensionDNRStore._getStoreDataClassForTesting();
+
+ let sandbox = sinon.createSandbox();
+ const schemasNormalizeSpy = sandbox.spy(Schemas, "normalize");
+ const ruleValidatorAddRulesSpy = sandbox.spy(
+ ExtensionDNR.RuleValidator.prototype,
+ "addRules"
+ );
+ const deserializeRuleSpy = sandbox.spy(
+ ExtensionDNR.RuleValidator,
+ "deserializeRule"
+ );
+ const clearLastUpdateTagPrefSpy = sandbox.spy(
+ StoreData,
+ "clearLastUpdateTagPref"
+ );
+ const scheduleCacheDataSaveSpy = sandbox.spy(
+ dnrStore,
+ "scheduleCacheDataSave"
+ );
+
+ resetTelemetryData();
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+ await ExtensionDNR.ensureInitialized(extension.extension);
+
+ if (expectLoadedFromCache) {
+ assertDNRTelemetryMetricsSamplesCount(
+ [
+ {
+ metric: "startupCacheReadSize",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_READ_BYTES",
+ mirroredType: "histogram",
+ expectedSamplesCount: 1,
+ },
+ {
+ metric: "startupCacheReadTime",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_READ_MS",
+ mirroredType: "histogram",
+ expectedSamplesCount: 1,
+ },
+ ],
+ "after app startup and expected startup cache hit"
+ );
+ assertDNRTelemetryMetricsGetValueEq(
+ [
+ {
+ metric: "startupCacheEntries",
+ label: "hit",
+ expectedGetValue: 1,
+ mirroredName: "extensions.apis.dnr.startup_cache_entries",
+ mirroredType: "keyedScalar",
+ },
+ ],
+ "after app startup and expected startup cache hit"
+ );
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ },
+ {
+ metric: "startupCacheEntries",
+ label: "miss",
+ mirroredName: "extensions.apis.dnr.startup_cache_entries",
+ mirroredType: "keyedScalar",
+ },
+ ],
+ "after DNR store loaded startup cache data"
+ );
+ } else {
+ assertDNRTelemetryMetricsSamplesCount(
+ [
+ {
+ metric: "validateRulesTime",
+ mirroredName: "WEBEXT_DNR_VALIDATE_RULES_MS",
+ mirroredType: "histogram",
+ expectedSamplesCount: 1,
+ },
+ {
+ metric: "startupCacheReadSize",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_READ_BYTES",
+ mirroredType: "histogram",
+ expectedSamplesCount: 1,
+ },
+ {
+ metric: "startupCacheReadTime",
+ mirroredName: "WEBEXT_DNR_STARTUPCACHE_READ_MS",
+ mirroredType: "histogram",
+ expectedSamplesCount: 1,
+ },
+ ],
+ "after app startup and expected startup cache miss"
+ );
+ assertDNRTelemetryMetricsGetValueEq(
+ [
+ {
+ metric: "startupCacheEntries",
+ label: "miss",
+ expectedGetValue: 1,
+ mirroredName: "extensions.apis.dnr.startup_cache_entries",
+ mirroredType: "keyedScalar",
+ },
+ ],
+ "after app startup and expected startup cache miss"
+ );
+ assertDNRTelemetryMetricsNoSamples(
+ [
+ {
+ metric: "startupCacheEntries",
+ label: "hit",
+ mirroredName: "extensions.apis.dnr.startup_cache_entries",
+ mirroredType: "keyedScalar",
+ },
+ ],
+ "after DNR store loaded startup cache data"
+ );
+ }
+
+ Assert.equal(
+ scheduleCacheDataSaveSpy.called,
+ !expectLoadedFromCache,
+ "scheduleCacheDataSave to not be called when the extension DNR rules are initialized from startup cache data"
+ );
+
+ Assert.equal(
+ clearLastUpdateTagPrefSpy.callCount,
+ expectClearLastUpdateTagPref ? 1 : 0,
+ "Expect clearLastUpdateTagPrefSpy to have been called the expected number of times"
+ );
+ if (expectClearLastUpdateTagPref === true) {
+ Assert.ok(
+ clearLastUpdateTagPrefSpy.calledWith(extension.uuid),
+ "Expect clearLastUpdateTagPrefSpy to have been called with the test extension uuid"
+ );
+ }
+
+ Assert.equal(
+ schemasNormalizeSpy.calledWith(
+ sinon.match.any,
+ sinon.match("declarativeNetRequest.Rule"),
+ sinon.match.any
+ ),
+ !expectLoadedFromCache,
+ `Expect DNR rules to ${
+ expectLoadedFromCache ? "NOT " : ""
+ }be going through Schemas.normalize`
+ );
+
+ Assert.equal(
+ ruleValidatorAddRulesSpy.called,
+ !expectLoadedFromCache,
+ `Expect DNR rules to ${
+ expectLoadedFromCache ? "NOT " : ""
+ }be going through RuleValidator addRules`
+ );
+
+ Assert.equal(
+ deserializeRuleSpy.called,
+ expectLoadedFromCache,
+ `Expect RuleValidator.deserializeRule to ${
+ expectLoadedFromCache ? "NOT " : ""
+ }be called to convert StartupCache data back into Rule class instances`
+ );
+
+ await assertDNRStoreData(dnrStore, extension, {
+ ruleset_1: getSchemaNormalizedRules(extension, RULESET_1_DATA),
+ });
+
+ sandbox.restore();
+ };
+
+ const expectedLastUpdateTag = ExtensionDNRStore._getLastUpdateTag(
+ extension.uuid
+ );
+
+ Assert.ok(
+ typeof expectedLastUpdateTag == "string" && !!expectedLastUpdateTag.length,
+ `Expect lastUpdateTag for ${extension.id} to be set to a non empty string: ${expectedLastUpdateTag}`
+ );
+
+ Assert.equal(
+ ExtensionDNRStore._getLastUpdateTag(temporarilyInstalledExt.uuid),
+ null,
+ `Expect no lastUpdateTag value set for temporarily installed extensions`
+ );
+
+ await assertDNRStoreDataLoadOnStartup({
+ expectLoadedFromCache: true,
+ expectClearLastUpdateTagPref: false,
+ });
+
+ {
+ const { buffer } = await IOUtils.read(cacheFile);
+ const decodedData = aomStartup.decodeBlob(buffer);
+ Assert.equal(
+ expectedLastUpdateTag,
+ decodedData?.cacheData.get(extension.uuid).lastUpdateTag,
+ "Expect cacheData entry's lastUpdateTag to match the value stored in the related pref"
+ );
+ Assert.equal(
+ decodedData?.cacheData.has(temporarilyInstalledExt.uuid),
+ false,
+ "Expect no cache data entry for temporarily installed extensions"
+ );
+
+ info("Confirm startupCache data dropped if last tag pref value mismatches");
+ ExtensionDNRStore._storeLastUpdateTag(
+ extension.uuid,
+ "mismatching-tag-value"
+ );
+ Assert.notEqual(
+ ExtensionDNRStore._getLastUpdateTag(extension.uuid),
+ decodedData?.cacheData.get(extension.uuid).lastUpdateTag,
+ "Expect cacheData.lastDNRStoreUpdateTag to NOT match the tampered value stored in the related pref"
+ );
+ }
+
+ await assertDNRStoreDataLoadOnStartup({
+ expectLoadedFromCache: false,
+ expectClearLastUpdateTagPref: true,
+ });
+
+ info(
+ "Verify that startupCache data mismatching with the StoreData schema version is being dropped"
+ );
+ await dnrStore.waitSaveCacheDataForTesting();
+ await assertDNRStoreDataLoadOnStartup({
+ expectLoadedFromCache: true,
+ expectClearLastUpdateTagPref: false,
+ });
+
+ {
+ info("Tamper the StoreData version in the startupCache data");
+ const { buffer } = await IOUtils.read(cacheFile);
+ const decodedData = aomStartup.decodeBlob(buffer);
+ decodedData.cacheData.get(extUUID).schemaVersion = -1;
+ await IOUtils.write(
+ cacheFile,
+ new Uint8Array(aomStartup.encodeBlob(decodedData))
+ );
+ }
+
+ await assertDNRStoreDataLoadOnStartup({
+ expectLoadedFromCache: false,
+ expectClearLastUpdateTagPref: true,
+ });
+
+ info(
+ "Verify that startupCache data mismatching with the extension version is being dropped"
+ );
+ await dnrStore.waitSaveCacheDataForTesting();
+ await assertDNRStoreDataLoadOnStartup({
+ expectLoadedFromCache: true,
+ expectClearLastUpdateTagPref: false,
+ });
+
+ {
+ info("Tamper the extension version in the startupCache data");
+ const { buffer } = await IOUtils.read(cacheFile);
+ const decodedData = aomStartup.decodeBlob(buffer);
+ decodedData.cacheData.get(extUUID).extVersion = "0.1";
+ await IOUtils.write(
+ cacheFile,
+ new Uint8Array(aomStartup.encodeBlob(decodedData))
+ );
+ }
+ await assertDNRStoreDataLoadOnStartup({
+ expectLoadedFromCache: false,
+ expectClearLastUpdateTagPref: true,
+ });
+
+ await extension.unload();
+
+ Assert.equal(
+ ExtensionDNRStore._getLastUpdateTag(extension.uuid),
+ null,
+ "LastUpdateTag pref should have been removed after addon uninstall"
+ );
+});
+
+add_task(async function test_detect_and_reschedule_save_cache_on_new_changes() {
+ const rule_resources = [
+ {
+ id: "ruleset_1",
+ enabled: true,
+ path: "ruleset_1.json",
+ },
+ ];
+ const files = {
+ "ruleset_1.json": JSON.stringify(RULESET_1_DATA),
+ };
+
+ let dnrStore = ExtensionDNRStore._getStoreForTesting();
+ let sandboxStore = sinon.createSandbox();
+ const spyScheduleCacheDataSave = sandboxStore.spy(
+ dnrStore,
+ "scheduleCacheDataSave"
+ );
+
+ let extension;
+ const tamperedLastUpdateTag = Services.uuid.generateUUID().toString();
+ let resolvePromiseSaveCacheRescheduled;
+ let promiseSaveCacheRescheduled = new Promise(resolve => {
+ resolvePromiseSaveCacheRescheduled = resolve;
+ });
+ const realDetectStartupCacheDataChanged =
+ dnrStore.detectStartupCacheDataChanged.bind(dnrStore);
+ const stubDetectCacheDataChanges = sandboxStore.stub(
+ dnrStore,
+ "detectStartupCacheDataChanged"
+ );
+
+ stubDetectCacheDataChanges.callsFake(seenLastUpdateTags => {
+ const extData = dnrStore._data.get(extension.extension.uuid);
+ Assert.ok(extData, "Got StoreData instance for the test extension");
+ Assert.ok(
+ typeof extData.lastUpdateTag === "string" &&
+ !!extData.lastUpdateTag.length,
+ "Expect a non empty lastUpdateTag assigned to the extension StoreData"
+ );
+ Assert.deepEqual(
+ Array.from(seenLastUpdateTags),
+ [extData.lastUpdateTag],
+ "Expects the extension storeData lastUpdateTag to have been seen"
+ );
+ if (stubDetectCacheDataChanges.callCount == 1) {
+ Assert.notEqual(
+ extData.lastUpdateTag,
+ tamperedLastUpdateTag,
+ "New tampered lastUpdateTag should not be equal to the one already set"
+ );
+ extData.lastUpdateTag = tamperedLastUpdateTag;
+ Assert.equal(
+ realDetectStartupCacheDataChanged(seenLastUpdateTags),
+ true,
+ "Expect dnrStore.detectStartupCacheDataChanged to detect a change"
+ );
+ return true;
+ }
+ Assert.equal(
+ realDetectStartupCacheDataChanged(seenLastUpdateTags),
+ false,
+ "Expect dnrStore.detectStartupCacheDataChanged to NOT have detected any change"
+ );
+
+ Promise.resolve().then(resolvePromiseSaveCacheRescheduled);
+ return false;
+ });
+
+ extension = ExtensionTestUtils.loadExtension(
+ getDNRExtension({
+ id: "test-reschedule-save-on-detected-changes@test",
+ rule_resources,
+ files,
+ })
+ );
+
+ await extension.startup();
+ info(
+ "Wait for DNR initialization completed for the permanently installed extension"
+ );
+ await ExtensionDNR.ensureInitialized(extension.extension);
+ info("Wait for the saveCacheDataNow task to have been rescheduled");
+ await promiseSaveCacheRescheduled;
+
+ Assert.equal(
+ spyScheduleCacheDataSave.callCount,
+ 2,
+ "Expect ExtensionDNRStore scheduleCacheDataSave method to have been called twice"
+ );
+ Assert.equal(
+ stubDetectCacheDataChanges.callCount,
+ 2,
+ "Expect ExtensionDNRStore detectStartupCacheDataChanged method to have been called twice"
+ );
+
+ sandboxStore.restore();
+
+ await extension.unload();
+});