summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js')
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js231
1 files changed, 231 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js
new file mode 100644
index 0000000000..92bf61dbde
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * @fileOverview Tests the MLBF and RemoteSettings synchronization logic.
+ */
+
+Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true);
+
+const { Downloader } = ChromeUtils.importESModule(
+ "resource://services-settings/Attachments.sys.mjs"
+);
+
+const ExtensionBlocklistMLBF = getExtensionBlocklistMLBF();
+
+// This test needs to interact with the RemoteSettings client.
+ExtensionBlocklistMLBF.ensureInitialized();
+
+add_task(async function fetch_invalid_mlbf_record() {
+ let invalidRecord = {
+ attachment: { size: 1, hash: "definitely not valid" },
+ generation_time: 1,
+ };
+
+ // _fetchMLBF(invalidRecord) may succeed if there is a MLBF dump packaged with
+ // the application. This test intentionally hides the actual path to get
+ // deterministic results. To check whether the dump is correctly registered,
+ // run test_blocklist_mlbf_dump.js
+
+ // Forget about the packaged attachment.
+ Downloader._RESOURCE_BASE_URL = "invalid://bogus";
+ // NetworkError is expected here. The JSON.parse error could be triggered via
+ // _baseAttachmentsURL < downloadAsBytes < download < download < _fetchMLBF if
+ // the request to services.settings.server ("data:,#remote-settings-dummy/v1")
+ // is fulfilled (but with invalid JSON). That request is not expected to be
+ // fulfilled in the first place, but that is not a concern of this test.
+ // This test passes if _fetchMLBF() rejects when given an invalid record.
+ await Assert.rejects(
+ ExtensionBlocklistMLBF._fetchMLBF(invalidRecord),
+ /NetworkError|SyntaxError: JSON\.parse/,
+ "record not found when there is no packaged MLBF"
+ );
+});
+
+// Other tests can mock _testMLBF, so let's verify that it works as expected.
+add_task(async function fetch_valid_mlbf() {
+ await ExtensionBlocklistMLBF._client.db.saveAttachment(
+ ExtensionBlocklistMLBF.RS_ATTACHMENT_ID,
+ { record: MLBF_RECORD, blob: await load_mlbf_record_as_blob() }
+ );
+
+ const result = await ExtensionBlocklistMLBF._fetchMLBF(MLBF_RECORD);
+ Assert.equal(result.cascadeHash, MLBF_RECORD.attachment.hash, "hash OK");
+ Assert.equal(result.generationTime, MLBF_RECORD.generation_time, "time OK");
+ Assert.ok(result.cascadeFilter.has("@blocked:1"), "item blocked");
+ Assert.ok(!result.cascadeFilter.has("@unblocked:2"), "item not blocked");
+
+ const result2 = await ExtensionBlocklistMLBF._fetchMLBF({
+ attachment: { size: 1, hash: "invalid" },
+ generation_time: Date.now(),
+ });
+ Assert.equal(
+ result2.cascadeHash,
+ MLBF_RECORD.attachment.hash,
+ "The cached MLBF should be used when the attachment is invalid"
+ );
+
+ // The attachment is kept in the database for use by the next test task.
+});
+
+// Test that results of the public API are consistent with the MLBF file.
+add_task(async function public_api_uses_mlbf() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+ await promiseStartupManager();
+
+ const blockedAddon = {
+ id: "@blocked",
+ version: "1",
+ signedDate: new Date(0), // a date in the past, before MLBF's generationTime.
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ };
+ const nonBlockedAddon = {
+ id: "@unblocked",
+ version: "2",
+ signedDate: new Date(0), // a date in the past, before MLBF's generationTime.
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ };
+
+ await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [MLBF_RECORD] });
+
+ Assert.deepEqual(
+ await Blocklist.getAddonBlocklistEntry(blockedAddon),
+ {
+ state: Ci.nsIBlocklistService.STATE_BLOCKED,
+ url: "https://addons.mozilla.org/en-US/xpcshell/blocked-addon/@blocked/1/",
+ },
+ "Blocked addon should have blocked entry"
+ );
+
+ Assert.deepEqual(
+ await Blocklist.getAddonBlocklistEntry(nonBlockedAddon),
+ null,
+ "Non-blocked addon should not be blocked"
+ );
+
+ Assert.equal(
+ await Blocklist.getAddonBlocklistState(blockedAddon),
+ Ci.nsIBlocklistService.STATE_BLOCKED,
+ "Blocked entry should have blocked state"
+ );
+
+ Assert.equal(
+ await Blocklist.getAddonBlocklistState(nonBlockedAddon),
+ Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ "Non-blocked entry should have unblocked state"
+ );
+
+ // Note: Blocklist collection and attachment carries over to the next test.
+});
+
+// Verifies that the metadata (time of validity) of an updated MLBF record is
+// correctly used, even if the MLBF itself has not changed.
+add_task(async function fetch_updated_mlbf_same_hash() {
+ const recordUpdate = {
+ ...MLBF_RECORD,
+ generation_time: MLBF_RECORD.generation_time + 1,
+ };
+ const blockedAddonUpdate = {
+ id: "@blocked",
+ version: "1",
+ signedDate: new Date(recordUpdate.generation_time),
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ };
+
+ // The blocklist already includes "@blocked:1", but the last specified
+ // generation time is MLBF_RECORD.generation_time. So the addon cannot be
+ // blocked, because the block decision could be a false positive.
+ Assert.equal(
+ await Blocklist.getAddonBlocklistState(blockedAddonUpdate),
+ Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ "Add-on not blocked before blocklist update"
+ );
+
+ await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [recordUpdate] });
+ // The MLBF is now known to apply to |blockedAddonUpdate|.
+
+ Assert.equal(
+ await Blocklist.getAddonBlocklistState(blockedAddonUpdate),
+ Ci.nsIBlocklistService.STATE_BLOCKED,
+ "Add-on blocked after update"
+ );
+
+ // Note: Blocklist collection and attachment carries over to the next test.
+});
+
+// Checks the remaining cases of database corruption that haven't been handled
+// before.
+add_task(async function handle_database_corruption() {
+ const blockedAddon = {
+ id: "@blocked",
+ version: "1",
+ signedDate: new Date(0), // a date in the past, before MLBF's generationTime.
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ };
+ async function checkBlocklistWorks() {
+ Assert.equal(
+ await Blocklist.getAddonBlocklistState(blockedAddon),
+ Ci.nsIBlocklistService.STATE_BLOCKED,
+ "Add-on should be blocked by the blocklist"
+ );
+ }
+
+ let fetchCount = 0;
+ const originalFetchMLBF = ExtensionBlocklistMLBF._fetchMLBF;
+ ExtensionBlocklistMLBF._fetchMLBF = function () {
+ ++fetchCount;
+ return originalFetchMLBF.apply(this, arguments);
+ };
+
+ // In the fetch_invalid_mlbf_record we checked that a cached / packaged MLBF
+ // attachment is used as a fallback when the record is invalid. Here we also
+ // check that there is a fallback when there is no record at all.
+
+ // Include a dummy record in the list, to prevent RemoteSettings from
+ // importing a JSON dump with unexpected records.
+ await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [{}] });
+ Assert.equal(fetchCount, 1, "MLBF read once despite bad record");
+ // When the collection is empty, the last known MLBF should be used anyway.
+ await checkBlocklistWorks();
+ Assert.equal(fetchCount, 1, "MLBF not read again by blocklist query");
+
+ // Now we also remove the cached file...
+ await ExtensionBlocklistMLBF._client.db.saveAttachment(
+ ExtensionBlocklistMLBF.RS_ATTACHMENT_ID,
+ null
+ );
+ Assert.equal(fetchCount, 1, "MLBF not read again after attachment deletion");
+ // Deleting the file shouldn't cause issues because the MLBF is loaded once
+ // and then kept in memory.
+ await checkBlocklistWorks();
+ Assert.equal(fetchCount, 1, "MLBF not read again by blocklist query 2");
+
+ // Force an update while we don't have any blocklist data nor cache.
+ await ExtensionBlocklistMLBF._onUpdate();
+ Assert.equal(fetchCount, 2, "MLBF read again at forced update");
+ // As a fallback, continue to use the in-memory version of the blocklist.
+ await checkBlocklistWorks();
+ Assert.equal(fetchCount, 2, "MLBF not read again by blocklist query 3");
+
+ // Memory gone, e.g. after a browser restart.
+ delete ExtensionBlocklistMLBF._mlbfData;
+ delete ExtensionBlocklistMLBF._stashes;
+ Assert.equal(
+ await Blocklist.getAddonBlocklistState(blockedAddon),
+ Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ "Blocklist can't work if all blocklist data is gone"
+ );
+ Assert.equal(fetchCount, 3, "MLBF read again after restart/cleared cache");
+ Assert.equal(
+ await Blocklist.getAddonBlocklistState(blockedAddon),
+ Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ "Blocklist can still not work if all blocklist data is gone"
+ );
+ // Ideally, the client packages a dump. But if the client did not package the
+ // dump, then it should not be trying to read the data over and over again.
+ Assert.equal(fetchCount, 3, "MLBF not read again despite absence of MLBF");
+
+ ExtensionBlocklistMLBF._fetchMLBF = originalFetchMLBF;
+});