diff options
Diffstat (limited to 'toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js')
-rw-r--r-- | toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js | 500 |
1 files changed, 500 insertions, 0 deletions
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js b/toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js new file mode 100644 index 0000000000..b3416b227a --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js @@ -0,0 +1,500 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { AMBrowserExtensionsImport } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); + +const mockAddonRepository = ({ + addons = [], + expectedBrowserID = null, + expectedExtensionIDs = null, + matchedIDs = [], + unmatchedIDs = [], +}) => { + return { + async getMappedAddons(browserID, extensionIDs) { + if (expectedBrowserID) { + Assert.equal(browserID, expectedBrowserID, "expected browser ID"); + } + if (expectedExtensionIDs) { + Assert.deepEqual( + extensionIDs, + expectedExtensionIDs, + "expected extension IDs" + ); + } + + return Promise.resolve({ + addons, + matchedIDs, + unmatchedIDs, + }); + }, + }; +}; + +const assertStageInstallsResult = (result, importedAddonIDs) => { + // Sort the results to always assert the elements in the same order. + result.importedAddonIDs.sort(); + Assert.deepEqual(result, { importedAddonIDs }, "expected results"); + Assert.ok( + AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected pending imported add-ons" + ); +}; + +const cancelInstalls = async importedAddonIDs => { + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-cancelled" + ); + // We want to verify that we received a `onInstallCancelled` event per + // (cancelled) install (i.e. per imported add-on). + const cancelledPromises = importedAddonIDs.map(id => + AddonTestUtils.promiseInstallEvent( + "onInstallCancelled", + (install, cancelledByUser) => { + Assert.equal(cancelledByUser, false, "Not user-cancelled"); + return install.addon.id == id; + } + ) + ); + await AMBrowserExtensionsImport.cancelInstalls(); + await Promise.all([promiseTopic, ...cancelledPromises]); + Assert.ok( + !AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected no pending imported add-ons" + ); +}; + +const TEST_SERVER = createHttpServer({ hosts: ["example.com"] }); + +const ADDONS = { + ext1: { + manifest: { + name: "Ext 1", + version: "1.0", + browser_specific_settings: { gecko: { id: "ff@ext-1" } }, + }, + }, + ext2: { + manifest: { + name: "Ext 2", + version: "1.0", + browser_specific_settings: { gecko: { id: "ff@ext-2" } }, + }, + }, +}; +// Populated in `setup()`. +const XPIS = {}; +// Populated in `setup()`. +const ADDON_SEARCH_RESULTS = {}; + +AddonTestUtils.init(this); +AddonTestUtils.overrideCertDB(); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_setup(async function setup() { + for (const [name, data] of Object.entries(ADDONS)) { + XPIS[name] = AddonTestUtils.createTempWebExtensionFile(data); + TEST_SERVER.registerFile(`/addons/${name}.xpi`, XPIS[name]); + + ADDON_SEARCH_RESULTS[name] = { + id: data.manifest.browser_specific_settings.gecko.id, + name: data.name, + version: data.version, + sourceURI: Services.io.newURI(`http://example.com/addons/${name}.xpi`), + icons: {}, + }; + } + + await AddonTestUtils.promiseStartupManager(); + + // FOG needs a profile directory to put its data in. + const profileDir = do_get_profile(); + + // FOG needs to be initialized in order for data to flow. + Services.fog.initializeFOG(); + + // When we stage installs and then cancel them, `XPIInstall` won't be able to + // remove the staging directory (which is expected to be empty) until the + // next restart. This causes an `AddonTestUtils` assertion to fail because we + // don't expect any staging directory at the end of the tests. That's why we + // remove this directory in the cleanup function defined below. + // + // We only remove the staging directory and that will only works if the + // directory is empty, otherwise an unchaught error will be thrown (on + // purpose). + registerCleanupFunction(() => { + const stagingDir = profileDir.clone(); + stagingDir.append("extensions"); + stagingDir.append("staged"); + stagingDir.exists() && stagingDir.remove(/* recursive */ false); + + // Clear the add-on repository override. + AMBrowserExtensionsImport._addonRepository = null; + }); +}); + +add_task(async function test_stage_and_complete_installs() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + let promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + // Make sure the prompt handler is the one from `AMBrowserExtensionsImport` + // since we don't want to show a permission prompt during an import. + for (const install of AMBrowserExtensionsImport._pendingInstallsMap.values()) { + Assert.equal( + install.promptHandler, + AMBrowserExtensionsImport._installPromptHandler, + "expected prompt handler to be the one set by AMBrowserExtensionsImport" + ); + } + + promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-complete" + ); + const endedPromises = importedAddonIDs.map(id => + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id == id + ) + ); + await AMBrowserExtensionsImport.completeInstalls(); + await Promise.all([promiseTopic, ...endedPromises]); + + Assert.ok( + !AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected no pending imported add-ons" + ); + Assert.ok( + !AMBrowserExtensionsImport._canCompleteOrCancelInstalls && + !AMBrowserExtensionsImport._importInProgress, + "expected internal state to be consistent" + ); + + for (const id of importedAddonIDs) { + const addon = await AddonManager.getAddonByID(id); + Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`); + await addon.uninstall(); + } +}); + +add_task(async function test_stage_and_cancel_installs() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + await cancelInstalls(importedAddonIDs); +}); + +add_task(async function test_stageInstalls_telemetry() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + const unmatchedIDs = ["unmatched-1", "unmatched-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + matchedIDs: ["ext-1", "ext-2"], + unmatchedIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + Assert.deepEqual( + Glean.browserMigration.matchedExtensions.testGetValue(), + extensionIDs + ); + Assert.deepEqual( + Glean.browserMigration.unmatchedExtensions.testGetValue(), + unmatchedIDs + ); + + await cancelInstalls(importedAddonIDs); +}); + +add_task(async function test_call_stageInstalls_twice() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + // Only return one extension. + addons: Object.values(ADDON_SEARCH_RESULTS).slice(0, 1), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1"]; + + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + let result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + await Assert.rejects( + AMBrowserExtensionsImport.stageInstalls(browserID, []), + /Cannot stage installs because there are pending imported add-ons/, + "expected rejection because there are pending imported add-ons" + ); + + // Cancel the installs for the previous import. + await cancelInstalls(importedAddonIDs); + + // We should now be able to stage installs again. + result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + assertStageInstallsResult(result, importedAddonIDs); + + await cancelInstalls(importedAddonIDs); +}); + +add_task(async function test_call_stageInstalls_no_addons() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-123456"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + // Returns no mapped add-ons. + addons: [], + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + + Assert.deepEqual(result, { importedAddonIDs: [] }, "expected result"); + Assert.ok( + !AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected no pending imported add-ons" + ); + Assert.ok( + !AMBrowserExtensionsImport._canCompleteOrCancelInstalls && + !AMBrowserExtensionsImport._importInProgress, + "expected internal state to be consistent" + ); +}); + +add_task(async function test_import_twice() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + let promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + let result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + // Finalize the installs. + promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-complete" + ); + const endedPromises = importedAddonIDs.map(id => + AddonTestUtils.promiseInstallEvent( + "onInstallEnded", + install => install.addon.id == id + ) + ); + await AMBrowserExtensionsImport.completeInstalls(); + await Promise.all([promiseTopic, ...endedPromises]); + + // Try to import the same add-ons again. Because these add-ons are already + // installed, we shouldn't re-import them again. + result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + Assert.deepEqual(result, { importedAddonIDs: [] }, "expected result"); + Assert.ok( + !AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected no pending imported add-ons" + ); + Assert.ok( + !AMBrowserExtensionsImport._canCompleteOrCancelInstalls && + !AMBrowserExtensionsImport._importInProgress, + "expected internal state to be consistent" + ); + + for (const id of importedAddonIDs) { + const addon = await AddonManager.getAddonByID(id); + Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`); + await addon.uninstall(); + } +}); + +add_task(async function test_call_cancelInstalls_without_pending_import() { + await Assert.rejects( + AMBrowserExtensionsImport.cancelInstalls(), + /No import in progress/, + "expected an error" + ); +}); + +add_task(async function test_call_completeInstalls_without_pending_import() { + await Assert.rejects( + AMBrowserExtensionsImport.completeInstalls(), + /No import in progress/, + "expected an error" + ); +}); + +add_task(async function test_stage_installs_with_download_aborted() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-2"]; + + // This listener will be triggered once (for the first imported add-on). Its + // goal is to cancel the download of an imported add-on and make sure it + // doesn't break everything. We still expect the second add-on to import to + // be staged for install. + const onNewInstall = AddonTestUtils.promiseInstallEvent( + "onNewInstall", + install => { + install.addListener({ + onDownloadStarted: () => false, + }); + return true; + } + ); + const promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + const result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await Promise.all([onNewInstall, promiseTopic]); + assertStageInstallsResult(result, importedAddonIDs); + + Assert.ok( + AMBrowserExtensionsImport.hasPendingImportedAddons, + "expected pending imported add-ons" + ); + Assert.ok( + AMBrowserExtensionsImport._canCompleteOrCancelInstalls && + AMBrowserExtensionsImport._importInProgress, + "expected internal state to be consistent" + ); + + // Let's cancel the pending installs. + await cancelInstalls(importedAddonIDs); +}); + +add_task(async function test_stageInstalls_then_restart_addonManager() { + const browserID = "some-browser-id"; + const extensionIDs = ["ext-1", "ext-2"]; + const EXPECTED_SOURCE_URI_SPECS = { + ["ff@ext-1"]: "http://example.com/addons/ext1.xpi", + ["ff@ext-2"]: "http://example.com/addons/ext2.xpi", + }; + AMBrowserExtensionsImport._addonRepository = mockAddonRepository({ + addons: Object.values(ADDON_SEARCH_RESULTS), + expectedBrowserID: browserID, + expectedExtensionIDs: extensionIDs, + }); + const importedAddonIDs = ["ff@ext-1", "ff@ext-2"]; + + let promiseTopic = TestUtils.topicObserved( + "webextension-imported-addons-pending" + ); + let result = await AMBrowserExtensionsImport.stageInstalls( + browserID, + extensionIDs + ); + await promiseTopic; + assertStageInstallsResult(result, importedAddonIDs); + + // We restart the add-ons manager to simulate a browser restart. It isn't + // quite the same but that should be enough. + await AddonTestUtils.promiseRestartManager(); + + for (const id of importedAddonIDs) { + const addon = await AddonManager.getAddonByID(id); + Assert.ok(addon.isActive, `expected add-on "${id}" to be enabled`); + // Verify that the sourceURI and installTelemetryInfo also match + // the values expected for the addons installed from the browser + // imports install flow. + Assert.deepEqual( + { + id: addon.id, + sourceURI: addon.sourceURI?.spec, + installTelemetryInfo: addon.installTelemetryInfo, + }, + { + id, + sourceURI: EXPECTED_SOURCE_URI_SPECS[id], + installTelemetryInfo: { + source: AMBrowserExtensionsImport.TELEMETRY_SOURCE, + }, + }, + "Got the expected AddonWrapper properties" + ); + await addon.uninstall(); + } +}); |