summaryrefslogtreecommitdiffstats
path: root/toolkit/components/normandy/test/unit/test_addon_unenroll.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/normandy/test/unit/test_addon_unenroll.js')
-rw-r--r--toolkit/components/normandy/test/unit/test_addon_unenroll.js307
1 files changed, 307 insertions, 0 deletions
diff --git a/toolkit/components/normandy/test/unit/test_addon_unenroll.js b/toolkit/components/normandy/test/unit/test_addon_unenroll.js
new file mode 100644
index 0000000000..3dd3c12c4b
--- /dev/null
+++ b/toolkit/components/normandy/test/unit/test_addon_unenroll.js
@@ -0,0 +1,307 @@
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+const { BranchedAddonStudyAction } = ChromeUtils.importESModule(
+ "resource://normandy/actions/BranchedAddonStudyAction.sys.mjs"
+);
+const { BaseAction } = ChromeUtils.importESModule(
+ "resource://normandy/actions/BaseAction.sys.mjs"
+);
+const { TelemetryEvents } = ChromeUtils.importESModule(
+ "resource://normandy/lib/TelemetryEvents.sys.mjs"
+);
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { AddonStudies } = ChromeUtils.importESModule(
+ "resource://normandy/lib/AddonStudies.sys.mjs"
+);
+
+/* import-globals-from utils.js */
+load("utils.js");
+
+NormandyTestUtils.init({ add_task });
+const { decorate_task } = NormandyTestUtils;
+
+const global = this;
+
+add_task(async () => {
+ ExtensionTestUtils.init(global);
+ AddonTestUtils.init(global);
+ AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ AddonTestUtils.overrideCertDB();
+ await AddonTestUtils.promiseStartupManager();
+
+ TelemetryEvents.init();
+});
+
+decorate_task(
+ withMockApiServer(),
+ AddonStudies.withStudies([]),
+ async function test_addon_unenroll({ server: apiServer }) {
+ const ID = "study@tests.mozilla.org";
+
+ // Create a test extension that uses webextension experiments to install
+ // an unenroll listener.
+ let xpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+
+ browser_specific_settings: {
+ gecko: { id: ID },
+ },
+
+ experiment_apis: {
+ study: {
+ schema: "schema.json",
+ parent: {
+ scopes: ["addon_parent"],
+ script: "api.js",
+ paths: [["study"]],
+ },
+ },
+ },
+ },
+
+ files: {
+ "schema.json": JSON.stringify([
+ {
+ namespace: "study",
+ events: [
+ {
+ name: "onStudyEnded",
+ type: "function",
+ },
+ ],
+ },
+ ]),
+
+ "api.js": () => {
+ // The code below is serialized into a file embedded in an extension.
+ // But by including it here as code, eslint can analyze it. However,
+ // this code runs in a different environment with different globals,
+ // the following two lines avoid false eslint warnings:
+ /* globals browser, ExtensionAPI */
+ /* eslint-disable-next-line no-shadow */
+ const { AddonStudies } = ChromeUtils.importESModule(
+ "resource://normandy/lib/AddonStudies.sys.mjs"
+ );
+ const { ExtensionCommon } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionCommon.sys.mjs"
+ );
+ this.study = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ study: {
+ onStudyEnded: new ExtensionCommon.EventManager({
+ context,
+ name: "study.onStudyEnded",
+ register: fire => {
+ AddonStudies.addUnenrollListener(
+ this.extension.id,
+ reason => fire.sync(reason)
+ );
+ return () => {};
+ },
+ }).api(),
+ },
+ };
+ }
+ };
+ },
+ },
+
+ background() {
+ browser.study.onStudyEnded.addListener(reason => {
+ browser.test.sendMessage("got-event", reason);
+ return new Promise(resolve => {
+ browser.test.onMessage.addListener(resolve);
+ });
+ });
+ },
+ });
+
+ const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+ server.registerFile("/study.xpi", xpi);
+
+ const API_ID = 999;
+ apiServer.registerPathHandler(
+ `/api/v1/extension/${API_ID}/`,
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(
+ JSON.stringify({
+ id: API_ID,
+ name: "Addon Unenroll Fixture",
+ xpi: "http://example.com/study.xpi",
+ extension_id: ID,
+ version: "1.0",
+ hash: CryptoUtils.getFileHash(xpi, "sha256"),
+ hash_algorithm: "sha256",
+ })
+ );
+ }
+ );
+
+ // Begin by telling Normandy to install the test extension above
+ // that uses a webextension experiment to register a blocking callback
+ // to be invoked when the study ends.
+ let extension = ExtensionTestUtils.expectExtension(ID);
+
+ const RECIPE_ID = 1;
+ const UNENROLL_REASON = "test-ending";
+ let action = new BranchedAddonStudyAction();
+ await action.processRecipe(
+ {
+ id: RECIPE_ID,
+ type: "addon-study",
+ arguments: {
+ slug: "addon-unenroll-test",
+ userFacingDescription: "A recipe to test add-on unenrollment",
+ userFacingName: "Add-on Unenroll Test",
+ isEnrollmentPaused: false,
+ branches: [
+ {
+ ratio: 1,
+ slug: "only",
+ extensionApiId: API_ID,
+ },
+ ],
+ },
+ },
+ BaseAction.suitability.FILTER_MATCH
+ );
+
+ await extension.awaitStartup();
+
+ let addon = await AddonManager.getAddonByID(ID);
+ ok(addon, "Extension is installed");
+
+ // Tell Normandy to end the study, the extension event should be fired.
+ let unenrollPromise = action.unenroll(RECIPE_ID, UNENROLL_REASON);
+
+ let receivedReason = await extension.awaitMessage("got-event");
+ info("Got onStudyEnded event in extension");
+ equal(receivedReason, UNENROLL_REASON, "Unenroll reason should be passed");
+
+ // The extension has not yet finished its unenrollment tasks, so it
+ // should not yet be uninstalled.
+ addon = await AddonManager.getAddonByID(ID);
+ ok(addon, "Extension has not yet been uninstalled");
+
+ // Once the extension does resolve the promise returned from the
+ // event listener, the uninstall can proceed.
+ extension.sendMessage("resolve");
+ await unenrollPromise;
+
+ addon = await AddonManager.getAddonByID(ID);
+ equal(
+ addon,
+ null,
+ "After resolving studyEnded promise, extension is uninstalled"
+ );
+ }
+);
+
+/* Test that a broken unenroll listener doesn't stop the add-on from being removed */
+decorate_task(
+ withMockApiServer(),
+ AddonStudies.withStudies([]),
+ async function test_addon_unenroll({ server: apiServer }) {
+ const ID = "study@tests.mozilla.org";
+
+ // Create a dummy webextension
+ // an unenroll listener that throws an error.
+ let xpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+
+ browser_specific_settings: {
+ gecko: { id: ID },
+ },
+ },
+ });
+
+ const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
+ server.registerFile("/study.xpi", xpi);
+
+ const API_ID = 999;
+ apiServer.registerPathHandler(
+ `/api/v1/extension/${API_ID}/`,
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(
+ JSON.stringify({
+ id: API_ID,
+ name: "Addon Fixture",
+ xpi: "http://example.com/study.xpi",
+ extension_id: ID,
+ version: "1.0",
+ hash: CryptoUtils.getFileHash(xpi, "sha256"),
+ hash_algorithm: "sha256",
+ })
+ );
+ }
+ );
+
+ // Begin by telling Normandy to install the test extension above that uses a
+ // webextension experiment to register a callback when the study ends that
+ // throws an error.
+ let extension = ExtensionTestUtils.expectExtension(ID);
+
+ const RECIPE_ID = 1;
+ const UNENROLL_REASON = "test-ending";
+ let action = new BranchedAddonStudyAction();
+ await action.processRecipe(
+ {
+ id: RECIPE_ID,
+ type: "addon-study",
+ arguments: {
+ slug: "addon-unenroll-test",
+ userFacingDescription: "A recipe to test add-on unenrollment",
+ userFacingName: "Add-on Unenroll Test",
+ isEnrollmentPaused: false,
+ branches: [
+ {
+ ratio: 1,
+ slug: "only",
+ extensionApiId: API_ID,
+ },
+ ],
+ },
+ },
+ BaseAction.suitability.FILTER_MATCH
+ );
+
+ await extension.startupPromise;
+
+ let addon = await AddonManager.getAddonByID(ID);
+ ok(addon, "Extension is installed");
+
+ let listenerDeferred = Promise.withResolvers();
+
+ AddonStudies.addUnenrollListener(ID, () => {
+ listenerDeferred.resolve();
+ throw new Error("This listener is busted");
+ });
+
+ // Tell Normandy to end the study, the extension event should be fired.
+ await action.unenroll(RECIPE_ID, UNENROLL_REASON);
+ await listenerDeferred;
+
+ addon = await AddonManager.getAddonByID(ID);
+ equal(
+ addon,
+ null,
+ "Extension is uninstalled even though it threw an exception in the callback"
+ );
+ }
+);