summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js')
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js500
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();
+ }
+});