forked from Floorp-Projects/Floorp
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bug 1620621 - Unit tests for MLBF-based blocklist r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D72631
- Loading branch information
Showing
8 changed files
with
439 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+32 Bytes
toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.bin
Binary file not shown.
12 changes: 12 additions & 0 deletions
12
toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/head.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,14 @@ | ||
// Appease eslint. | ||
/* import-globals-from ../head_addons.js */ | ||
|
||
const MLBF_RECORD = { | ||
id: "A blocklist entry that refers to a MLBF file", | ||
last_modified: 1, | ||
attachment: { | ||
size: 32, | ||
hash: "6af648a5d6ce6dbee99b0aab1780d24d204977a6606ad670d5372ef22fac1052", | ||
filename: "does-not-matter.bin", | ||
}, | ||
attachment_type: "bloomfilter-full", | ||
generation_time: 1577833200000, | ||
}; |
169 changes: 169 additions & 0 deletions
169
toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
/* Any copyright is dedicated to the Public Domain. | ||
* https://creativecommons.org/publicdomain/zero/1.0/ */ | ||
|
||
"use strict"; | ||
|
||
Services.prefs.setBoolPref("extensions.blocklist.useMLBF", true); | ||
|
||
const { ExtensionBlocklistMLBF } = ChromeUtils.import( | ||
"resource://gre/modules/Blocklist.jsm", | ||
null | ||
); | ||
|
||
createAppInfo("[email protected]", "XPCShell", "1", "1"); | ||
AddonTestUtils.useRealCertChecks = true; | ||
|
||
// A real, signed XPI for use in the test. | ||
const SIGNED_ADDON_XPI_FILE = do_get_file("../data/webext-implicit-id.xpi"); | ||
const SIGNED_ADDON_ID = "[email protected]"; | ||
const SIGNED_ADDON_VERSION = "1.0"; | ||
const SIGNED_ADDON_KEY = `${SIGNED_ADDON_ID}:${SIGNED_ADDON_VERSION}`; | ||
const SIGNED_ADDON_SIGN_TIME = 1459980789000; // notBefore of certificate. | ||
|
||
function mockMLBF({ blocked = [], notblocked = [], generationTime }) { | ||
// Mock _fetchMLBF to be able to have a deterministic cascade filter. | ||
ExtensionBlocklistMLBF._fetchMLBF = async () => { | ||
return { | ||
cascadeFilter: { | ||
has(blockKey) { | ||
if (blocked.includes(blockKey)) { | ||
return true; | ||
} | ||
if (notblocked.includes(blockKey)) { | ||
return false; | ||
} | ||
throw new Error(`Block entry must explicitly be listed: ${blockKey}`); | ||
}, | ||
}, | ||
generationTime, | ||
}; | ||
}; | ||
} | ||
|
||
add_task(async function setup() { | ||
await promiseStartupManager(); | ||
mockMLBF({}); | ||
await AddonTestUtils.loadBlocklistRawData({ | ||
extensionsMLBF: [MLBF_RECORD], | ||
}); | ||
}); | ||
|
||
// Checks: Initially unblocked, then blocked, then unblocked again. | ||
add_task(async function signed_xpi_initially_unblocked() { | ||
mockMLBF({ | ||
blocked: [], | ||
notblocked: [SIGNED_ADDON_KEY], | ||
generationTime: SIGNED_ADDON_SIGN_TIME + 1, | ||
}); | ||
await ExtensionBlocklistMLBF._onUpdate(); | ||
|
||
await promiseInstallFile(SIGNED_ADDON_XPI_FILE); | ||
|
||
let addon = await promiseAddonByID(SIGNED_ADDON_ID); | ||
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); | ||
|
||
mockMLBF({ | ||
blocked: [SIGNED_ADDON_KEY], | ||
notblocked: [], | ||
generationTime: SIGNED_ADDON_SIGN_TIME + 1, | ||
}); | ||
await ExtensionBlocklistMLBF._onUpdate(); | ||
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED); | ||
Assert.deepEqual( | ||
await Blocklist.getAddonBlocklistEntry(addon), | ||
{ | ||
state: Ci.nsIBlocklistService.STATE_BLOCKED, | ||
url: | ||
"https://addons.mozilla.org/en-US/xpcshell/blocked-addon/[email protected]/1.0/", | ||
}, | ||
"Blocked addon should have blocked entry" | ||
); | ||
|
||
mockMLBF({ | ||
blocked: [SIGNED_ADDON_KEY], | ||
notblocked: [], | ||
// MLBF generationTime is older, so "blocked" entry should not apply. | ||
generationTime: SIGNED_ADDON_SIGN_TIME - 1, | ||
}); | ||
await ExtensionBlocklistMLBF._onUpdate(); | ||
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); | ||
|
||
await addon.uninstall(); | ||
}); | ||
|
||
// Checks: Initially blocked on install, then unblocked. | ||
add_task(async function signed_xpi_blocked_on_install() { | ||
mockMLBF({ | ||
blocked: [SIGNED_ADDON_KEY], | ||
notblocked: [], | ||
generationTime: SIGNED_ADDON_SIGN_TIME + 1, | ||
}); | ||
await ExtensionBlocklistMLBF._onUpdate(); | ||
|
||
await promiseInstallFile(SIGNED_ADDON_XPI_FILE); | ||
let addon = await promiseAddonByID(SIGNED_ADDON_ID); | ||
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED); | ||
Assert.ok(addon.appDisabled, "Blocked add-on is disabled on install"); | ||
|
||
mockMLBF({ | ||
blocked: [], | ||
notblocked: [SIGNED_ADDON_KEY], | ||
generationTime: SIGNED_ADDON_SIGN_TIME - 1, | ||
}); | ||
await ExtensionBlocklistMLBF._onUpdate(); | ||
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); | ||
Assert.ok(!addon.appDisabled, "Re-enabled after unblock"); | ||
|
||
await addon.uninstall(); | ||
}); | ||
|
||
// An unsigned add-on cannot be blocked. | ||
add_task(async function unsigned_not_blocked() { | ||
const UNSIGNED_ADDON_ID = "[email protected]"; | ||
const UNSIGNED_ADDON_VERSION = "1.0"; | ||
const UNSIGNED_ADDON_KEY = `${UNSIGNED_ADDON_ID}:${UNSIGNED_ADDON_VERSION}`; | ||
mockMLBF({ | ||
blocked: [UNSIGNED_ADDON_KEY], | ||
notblocked: [], | ||
generationTime: SIGNED_ADDON_SIGN_TIME + 1, | ||
}); | ||
await ExtensionBlocklistMLBF._onUpdate(); | ||
|
||
let unsignedAddonFile = createTempWebExtensionFile({ | ||
manifest: { | ||
version: UNSIGNED_ADDON_VERSION, | ||
applications: { gecko: { id: UNSIGNED_ADDON_ID } }, | ||
}, | ||
}); | ||
|
||
// Unsigned add-ons can generally only be loaded as a temporary install. | ||
let [addon] = await Promise.all([ | ||
AddonManager.installTemporaryAddon(unsignedAddonFile), | ||
promiseWebExtensionStartup(UNSIGNED_ADDON_ID), | ||
]); | ||
Assert.equal(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); | ||
Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED); | ||
Assert.equal( | ||
await Blocklist.getAddonBlocklistState(addon), | ||
Ci.nsIBlocklistService.STATE_NOT_BLOCKED, | ||
"Unsigned temporary add-on is not blocked" | ||
); | ||
await addon.uninstall(); | ||
}); | ||
|
||
// To make sure that unsigned_not_blocked did not trivially pass, we also check | ||
// that add-ons can actually be blocked when installed as a temporary add-on. | ||
add_task(async function signed_temporary() { | ||
mockMLBF({ | ||
blocked: [SIGNED_ADDON_KEY], | ||
notblocked: [], | ||
generationTime: SIGNED_ADDON_SIGN_TIME + 1, | ||
}); | ||
await ExtensionBlocklistMLBF._onUpdate(); | ||
|
||
await Assert.rejects( | ||
AddonManager.installTemporaryAddon(SIGNED_ADDON_XPI_FILE), | ||
/Add-on [email protected] is not compatible with application version/, | ||
"Blocklisted add-on cannot be installed" | ||
); | ||
}); |
174 changes: 174 additions & 0 deletions
174
toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
/* 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.import( | ||
"resource://services-settings/Attachments.jsm" | ||
); | ||
|
||
const { ExtensionBlocklistMLBF } = ChromeUtils.import( | ||
"resource://gre/modules/Blocklist.jsm", | ||
null | ||
); | ||
|
||
// 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, | ||
}; | ||
|
||
let resultPromise = ExtensionBlocklistMLBF._fetchMLBF(invalidRecord); | ||
|
||
// TODO bug ...: When the MLBF is packaged with the application, this | ||
// assertion should be updated to pass. | ||
await Assert.rejects(resultPromise, /NetworkError/, "record not found"); | ||
|
||
// Forget about the packaged attachment. | ||
Downloader._RESOURCE_BASE_URL = "invalid://bogus"; | ||
await Assert.rejects( | ||
ExtensionBlocklistMLBF._fetchMLBF(invalidRecord), | ||
/NetworkError/, | ||
"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() { | ||
const url = Services.io.newFileURI( | ||
do_get_file("../data/mlbf-blocked1-unblocked2.bin") | ||
).spec; | ||
Cu.importGlobalProperties(["fetch"]); | ||
const blob = await (await fetch(url)).blob(); | ||
|
||
await ExtensionBlocklistMLBF._client.db.saveAttachment( | ||
ExtensionBlocklistMLBF.RS_ATTACHMENT_ID, | ||
{ record: JSON.parse(JSON.stringify(MLBF_RECORD)), 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("[email protected]", "XPCShell", "1", "1"); | ||
await promiseStartupManager(); | ||
|
||
const blockedAddon = { | ||
id: "@blocked", | ||
version: "1", | ||
signedState: 2, // = AddonManager.SIGNEDSTATE_SIGNED. | ||
signedDate: 0, // a date in the past, before MLBF's generationTime. | ||
}; | ||
const nonBlockedAddon = { | ||
id: "@unblocked", | ||
version: "2", | ||
signedState: 2, // = AddonManager.SIGNEDSTATE_SIGNED. | ||
signedDate: 0, // a date in the past, before MLBF's generationTime. | ||
}; | ||
|
||
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. | ||
}); | ||
|
||
// 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", | ||
signedState: 2, // = AddonManager.SIGNEDSTATE_SIGNED. | ||
signedDate: 0, // a date in the past, before MLBF's generationTime. | ||
}; | ||
async function checkBlocklistWorks() { | ||
Assert.equal( | ||
await Blocklist.getAddonBlocklistState(blockedAddon), | ||
Ci.nsIBlocklistService.STATE_BLOCKED, | ||
"Add-on should be blocked by the blocklist" | ||
); | ||
} | ||
|
||
// 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. | ||
|
||
await AddonTestUtils.loadBlocklistRawData({ extensionsMLBF: [] }); | ||
// When the collection is empty, the last known MLBF should be used anyway. | ||
await checkBlocklistWorks(); | ||
|
||
// Now we also remove the cached file... | ||
await ExtensionBlocklistMLBF._client.db.saveAttachment( | ||
ExtensionBlocklistMLBF.RS_ATTACHMENT_ID, | ||
null | ||
); | ||
// Deleting the file shouldn't cause issues because the MLBF is loaded once | ||
// and then kept in memory. | ||
await checkBlocklistWorks(); | ||
|
||
// Force an update while we don't have any blocklist data nor cache. | ||
await ExtensionBlocklistMLBF._onUpdate(); | ||
// As a fallback, continue to use the in-memory version of the blocklist. | ||
await checkBlocklistWorks(); | ||
|
||
// Memory gone, e.g. after a browser restart. | ||
delete ExtensionBlocklistMLBF._mlbfData; | ||
Assert.equal( | ||
await Blocklist.getAddonBlocklistState(blockedAddon), | ||
Ci.nsIBlocklistService.STATE_NOT_BLOCKED, | ||
"Blocklist can't work if all blocklist data is gone" | ||
); | ||
}); |
Oops, something went wrong.