Bug 1752979 - Tests for file changes during addon installs r=mixedpuppy
authorRob Wu <rob@robwu.nl>
Mon, 23 May 2022 12:39:10 +0000
changeset 618537 d8df5666ec7dab641fc5f321fac71d63229ca867
parent 618536 4aef479d5406144e414cc6302847b63a1fa8eb1e
child 618538 3fb319df49c63d4dced1dc5b9a823fd36abe8f68
push id163319
push userrob@robwu.nl
push dateMon, 23 May 2022 12:41:36 +0000
treeherderautoland@d8df5666ec7d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1752979
milestone102.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1752979 - Tests for file changes during addon installs r=mixedpuppy Differential Revision: https://phabricator.services.mozilla.com/D137693
toolkit/mozapps/extensions/test/xpcshell/test_install_file_change.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_file_change.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+/* globals browser */
+
+add_task(async function setup() {
+  await ExtensionTestUtils.startAddonManager();
+});
+
+async function createXPIWithID(addonId, version = "1.0") {
+  let xpiFile = await createTempWebExtensionFile({
+    manifest: {
+      version,
+      applications: { gecko: { id: addonId } },
+    },
+  });
+  return xpiFile;
+}
+
+const ERROR_PATTERN_INSTALL_FAIL = /Failed to install .+ from .+ to /;
+const ERROR_PATTERN_POSTPONE_FAIL = /Failed to postpone install of /;
+
+async function promiseInstallFail(install, expectedErrorPattern) {
+  let { messages } = await promiseConsoleOutput(async () => {
+    await Assert.rejects(
+      install.install(),
+      /^Error: Install failed: onInstallFailed$/
+    );
+  });
+  messages = messages.filter(msg => expectedErrorPattern.test(msg.message));
+  equal(messages.length, 1, "Expected log messages");
+  equal(install.state, AddonManager.STATE_INSTALL_FAILED);
+  equal(install.error, AddonManager.ERROR_FILE_ACCESS);
+  equal((await AddonManager.getAllInstalls()).length, 0, "no pending installs");
+}
+
+add_task(async function test_file_deleted() {
+  let xpiFile = await createXPIWithID("delete@me");
+  let install = await AddonManager.getInstallForFile(xpiFile);
+  equal(install.state, AddonManager.STATE_DOWNLOADED);
+
+  xpiFile.remove(false);
+
+  await promiseInstallFail(install, ERROR_PATTERN_INSTALL_FAIL);
+
+  equal(await AddonManager.getAddonByID("delete@me"), null);
+});
+
+add_task(async function test_file_emptied() {
+  let xpiFile = await createXPIWithID("empty@me");
+  let install = await AddonManager.getInstallForFile(xpiFile);
+  equal(install.state, AddonManager.STATE_DOWNLOADED);
+
+  await IOUtils.write(xpiFile.path, new Uint8Array());
+
+  await promiseInstallFail(install, ERROR_PATTERN_INSTALL_FAIL);
+
+  equal(await AddonManager.getAddonByID("empty@me"), null);
+});
+
+add_task(async function test_file_replaced() {
+  let xpiFile = await createXPIWithID("replace@me");
+  let install = await AddonManager.getInstallForFile(xpiFile);
+  equal(install.state, AddonManager.STATE_DOWNLOADED);
+
+  await IOUtils.copy(
+    (await createXPIWithID("replace@me", "2")).path,
+    xpiFile.path
+  );
+
+  await promiseInstallFail(install, ERROR_PATTERN_INSTALL_FAIL);
+
+  equal(await AddonManager.getAddonByID("replace@me"), null);
+});
+
+async function do_test_update_with_file_replaced(wantPostponeTest) {
+  const ADDON_ID = wantPostponeTest ? "postpone@me" : "update@me";
+  function backgroundWithPostpone() {
+    // The registration of this listener postpones the update.
+    browser.runtime.onUpdateAvailable.addListener(() => {
+      browser.test.fail("Unusable update should not call onUpdateAvailable");
+    });
+  }
+  await promiseInstallWebExtension({
+    manifest: {
+      version: "1.0",
+      applications: {
+        gecko: {
+          id: ADDON_ID,
+          update_url: `http://example.com/update-${ADDON_ID}.json`,
+        },
+      },
+    },
+    background: wantPostponeTest ? backgroundWithPostpone : () => {},
+  });
+
+  server.registerFile(
+    `/update-${ADDON_ID}.xpi`,
+    await createTempWebExtensionFile({
+      manifest: {
+        version: "2.0",
+        applications: { gecko: { id: ADDON_ID } },
+      },
+    })
+  );
+  AddonTestUtils.registerJSON(server, `/update-${ADDON_ID}.json`, {
+    addons: {
+      [ADDON_ID]: {
+        updates: [
+          {
+            version: "2.0",
+            update_link: `http://example.com/update-${ADDON_ID}.xpi`,
+          },
+        ],
+      },
+    },
+  });
+
+  // Setup completed, let's try to verify that file corruption halts the update.
+
+  let addon = await promiseAddonByID(ADDON_ID);
+  equal(addon.version, "1.0");
+
+  let update = await promiseFindAddonUpdates(
+    addon,
+    AddonManager.UPDATE_WHEN_USER_REQUESTED
+  );
+  let install = update.updateAvailable;
+  equal(install.version, "2.0");
+  equal(install.state, AddonManager.STATE_AVAILABLE);
+  equal(install.existingAddon, addon);
+  equal(install.file, null);
+
+  let promptCount = 0;
+  let didReplaceFile = false;
+  install.promptHandler = async function() {
+    ++promptCount;
+    equal(install.state, AddonManager.STATE_DOWNLOADED);
+    await IOUtils.copy(
+      (await createXPIWithID(ADDON_ID, "3")).path,
+      install.file.path
+    );
+    didReplaceFile = true;
+    equal(install.state, AddonManager.STATE_DOWNLOADED, "State not changed");
+  };
+
+  if (wantPostponeTest) {
+    await promiseInstallFail(install, ERROR_PATTERN_POSTPONE_FAIL);
+  } else {
+    await promiseInstallFail(install, ERROR_PATTERN_INSTALL_FAIL);
+  }
+
+  equal(promptCount, 1);
+  ok(didReplaceFile, "Replaced update with different file");
+
+  // Now verify that the add-on is still at the old version.
+  addon = await promiseAddonByID(ADDON_ID);
+  equal(addon.version, "1.0");
+
+  await addon.uninstall();
+}
+
+add_task(async function test_update_and_file_replaced() {
+  await do_test_update_with_file_replaced();
+});
+
+add_task(async function test_update_postponed_and_file_replaced() {
+  await do_test_update_with_file_replaced(/* wantPostponeTest = */ true);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -48,16 +48,17 @@ tags = webextensions
 [test_getInstallSourceFromHost.js]
 [test_gmpProvider.js]
 skip-if = appname != "firefox" || (os == "win" && processor == "aarch64") # bug 1536637
 [test_harness.js]
 [test_hidden.js]
 [test_install.js]
 [test_installOrigins.js]
 [test_install_cancel.js]
+[test_install_file_change.js]
 [test_install_icons.js]
 [test_installtrigger_schemes.js]
 [test_installtrigger_deprecation.js]
 [test_isDebuggable.js]
 [test_isReady.js]
 [test_loadManifest_isPrivileged.js]
 [test_locale.js]
 [test_moved_extension_metadata.js]