Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
/* Any copyright is dedicated to the Public Domain.
"use strict";
/**
* @fileOverview Tests the MLBF and RemoteSettings synchronization logic.
*/
Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true);
const { Downloader } = ChromeUtils.importESModule(
);
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.
// 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 { mlbf: 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 { mlbf: 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,
},
"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;
});