fune/toolkit/components/normandy/test/browser/browser_actions_AddonStudyAction.js
Mike Cooper 163557534c Bug 1440780 - Add Normandy action for add-on studies r=aswan
This ports the code from the Normandy server github repo to run as a local
action, instead of being fetched from the server.

The original code is here:
c0a8c53707/client/actions/opt-out-study

Differential Revision: https://phabricator.services.mozilla.com/D2973

--HG--
extra : moz-landing-system : lando
2018-08-20 20:37:35 +00:00

301 lines
10 KiB
JavaScript

"use strict";
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
ChromeUtils.import("resource://normandy/actions/AddonStudyAction.jsm", this);
ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
ChromeUtils.import("resource://normandy/lib/Uptake.jsm", this);
const FIXTURE_ADDON_ID = "normandydriver@example.com";
const FIXTURE_ADDON_URL = "http://example.com/browser/toolkit/components/normandy/test/browser/fixtures/normandy.xpi";
function addonStudyRecipeFactory(overrides = {}) {
let args = {
name: "Fake name",
description: "fake description",
addonUrl: "https://example.com/study.xpi",
};
if (Object.hasOwnProperty.call(overrides, "arguments")) {
args = Object.assign(args, overrides.arguments);
delete overrides.arguments;
}
return recipeFactory(Object.assign({ action: "addon-study", arguments: args }, overrides));
}
/**
* Test decorator that checks that the test cleans up all add-ons installed
* during the test. Likely needs to be the first decorator used.
*/
function ensureAddonCleanup(testFunction) {
return async function wrappedTestFunction(...args) {
const beforeAddons = new Set(await AddonManager.getAllAddons());
try {
await testFunction(...args);
} finally {
const afterAddons = new Set(await AddonManager.getAllAddons());
Assert.deepEqual(beforeAddons, afterAddons, "The add-ons should be same before and after the test");
}
};
}
// Test that enroll is not called if recipe is already enrolled
decorate_task(
ensureAddonCleanup,
AddonStudies.withStudies([studyFactory()]),
withSendEventStub,
async function enrollTwiceFail([study], sendEventStub) {
const recipe = recipeFactory({
id: study.recipeId,
type: "addon-study",
arguments: {
name: study.name,
description: study.description,
addonUrl: study.addonUrl,
},
});
const action = new AddonStudyAction();
const enrollSpy = sinon.spy(action, "enroll");
await action.runRecipe(recipe);
Assert.deepEqual(enrollSpy.args, [], "enroll should not be called");
Assert.deepEqual(sendEventStub.args, [], "no events should be sent");
},
);
// Test that if the add-on fails to install, the database is cleaned up and the
// error is correctly reported.
decorate_task(
ensureAddonCleanup,
withSendEventStub,
AddonStudies.withStudies([]),
async function enrollFailInstall(sendEventStub) {
const recipe = addonStudyRecipeFactory({ arguments: { addonUrl: "https://example.com/404.xpi" }});
const action = new AddonStudyAction();
await action.enroll(recipe);
const studies = await AddonStudies.getAll();
Assert.deepEqual(studies, [], "the study should not be in the database");
Assert.deepEqual(
sendEventStub.args,
[["enrollFailed", "addon_study", recipe.arguments.name, {reason: "download-failure"}]],
"An enrollFailed event should be sent",
);
}
);
// Test that in the case of a study add-on conflicting with a non-study add-on, the study does not enroll
decorate_task(
ensureAddonCleanup,
AddonStudies.withStudies([]),
withSendEventStub,
withInstalledWebExtension({ version: "0.1", id: FIXTURE_ADDON_ID }),
async function conflictingEnrollment(studies, sendEventStub, [installedAddonId, installedAddonFile]) {
is(installedAddonId, FIXTURE_ADDON_ID, "Generated, installed add-on should have the same ID as the fixture");
const addonUrl = FIXTURE_ADDON_URL;
const recipe = addonStudyRecipeFactory({ arguments: { name: "conflicting", addonUrl } });
const action = new AddonStudyAction();
await action.runRecipe(recipe);
const addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
is(addon.version, "0.1", "The installed add-on should not be replaced");
Assert.deepEqual(await AddonStudies.getAll(), [], "There should be no enrolled studies");
Assert.deepEqual(
sendEventStub.args,
[["enrollFailed", "addon_study", recipe.arguments.name, { reason: "conflicting-addon-id" }]],
"A enrollFailed event should be sent",
);
},
);
// Test a successful enrollment
decorate_task(
ensureAddonCleanup,
withSendEventStub,
AddonStudies.withStudies(),
async function successfulEnroll(sendEventStub, studies) {
const webExtStartupPromise = AddonTestUtils.promiseWebExtensionStartup(FIXTURE_ADDON_ID);
const addonUrl = FIXTURE_ADDON_URL;
let addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
is(addon, null, "Before enroll, the add-on is not installed");
const recipe = addonStudyRecipeFactory({ arguments: { name: "success", addonUrl } });
const action = new AddonStudyAction();
await action.runRecipe(recipe);
await webExtStartupPromise;
addon = await AddonManager.getAddonByID(FIXTURE_ADDON_ID);
ok(addon, "After start is called, the add-on is installed");
const study = await AddonStudies.get(recipe.id);
Assert.deepEqual(
study,
{
recipeId: recipe.id,
name: recipe.arguments.name,
description: recipe.arguments.description,
addonId: FIXTURE_ADDON_ID,
addonVersion: "1.0",
addonUrl,
active: true,
studyStartDate: study.studyStartDate,
},
"study data should be stored",
);
ok(study.studyStartDate, "a start date should be assigned");
is(study.studyEndDate, null, "an end date should not be assigned");
Assert.deepEqual(
sendEventStub.args,
[["enroll", "addon_study", recipe.arguments.name, { addonId: FIXTURE_ADDON_ID, addonVersion: "1.0" }]],
"an enrollment event should be sent",
);
// cleanup
await addon.uninstall();
},
);
// Test that unenrolling fails if the study doesn't exist
decorate_task(
ensureAddonCleanup,
AddonStudies.withStudies(),
async function unenrollNonexistent(studies) {
const action = new AddonStudyAction();
await Assert.rejects(
action.unenroll(42),
/no study found/i,
"unenroll should fail when no study exists"
);
}
);
// Test that unenrolling an inactive experiment fails
decorate_task(
ensureAddonCleanup,
AddonStudies.withStudies([
studyFactory({active: false}),
]),
withSendEventStub,
async ([study], sendEventStub) => {
const action = new AddonStudyAction();
await Assert.rejects(
action.unenroll(study.recipeId),
/cannot stop study.*already inactive/i,
"unenroll should fail when the requested study is inactive"
);
}
);
// test a successful unenrollment
const testStopId = "testStop@example.com";
decorate_task(
ensureAddonCleanup,
AddonStudies.withStudies([
studyFactory({active: true, addonId: testStopId, studyEndDate: null}),
]),
withInstalledWebExtension({id: testStopId}, /* expectUninstall: */ true),
withSendEventStub,
async function unenrollTest([study], [addonId, addonFile], sendEventStub) {
let addon = await AddonManager.getAddonByID(addonId);
ok(addon, "the add-on should be installed before unenrolling");
const action = new AddonStudyAction();
await action.unenroll(study.recipeId, "test-reason");
const newStudy = AddonStudies.get(study.recipeId);
is(!newStudy, false, "stop should mark the study as inactive");
ok(newStudy.studyEndDate !== null, "the study should have an end date");
addon = await AddonManager.getAddonByID(addonId);
is(addon, null, "the add-on should be uninstalled after unenrolling");
Assert.deepEqual(
sendEventStub.args,
[["unenroll", "addon_study", study.name, {
addonId,
addonVersion: study.addonVersion,
reason: "test-reason"
}]],
"an unenroll event should be sent",
);
},
);
// If the add-on for a study isn't installed, a warning should be logged, but the action is still successful
decorate_task(
ensureAddonCleanup,
AddonStudies.withStudies([
studyFactory({active: true, addonId: "missingAddon@example.com", studyEndDate: null}),
]),
withSendEventStub,
async function unenrollTest([study], sendEventStub) {
const action = new AddonStudyAction();
SimpleTest.waitForExplicitFinish();
SimpleTest.monitorConsole(() => SimpleTest.finish(), [{message: /could not uninstall addon/i}]);
await action.unenroll(study.recipeId);
Assert.deepEqual(
sendEventStub.args,
[["unenroll", "addon_study", study.name, {
addonId: study.addonId,
addonVersion: study.addonVersion,
reason: "unknown"
}]],
"an unenroll event should be sent",
);
SimpleTest.endMonitorConsole();
},
);
// Test that the action respects the study opt-out
decorate_task(
ensureAddonCleanup,
withSendEventStub,
withMockPreferences,
AddonStudies.withStudies([]),
async function testOptOut(sendEventStub, mockPreferences) {
mockPreferences.set("app.shield.optoutstudies.enabled", false);
const action = new AddonStudyAction();
is(action.state, AddonStudyAction.STATE_DISABLED, "the action should be disabled");
const enrollSpy = sinon.spy(action, "enroll");
const recipe = addonStudyRecipeFactory();
await action.runRecipe(recipe);
await action.finalize();
is(action.state, AddonStudyAction.STATE_FINALIZED, "the action should be finalized");
Assert.deepEqual(enrollSpy.args, [], "enroll should not be called");
Assert.deepEqual(sendEventStub.args, [], "no events should be sent");
},
);
// Test that the action does not execute paused recipes
decorate_task(
ensureAddonCleanup,
withSendEventStub,
AddonStudies.withStudies([]),
async function testOptOut(sendEventStub) {
const action = new AddonStudyAction();
const enrollSpy = sinon.spy(action, "enroll");
const recipe = addonStudyRecipeFactory({arguments: {isEnrollmentPaused: true}});
await action.runRecipe(recipe);
await action.finalize();
Assert.deepEqual(enrollSpy.args, [], "enroll should not be called");
Assert.deepEqual(sendEventStub.args, [], "no events should be sent");
},
);
// Test that enroll is not called if recipe is already enrolled
decorate_task(
ensureAddonCleanup,
AddonStudies.withStudies([studyFactory()]),
async function enrollTwiceFail([study]) {
const action = new AddonStudyAction();
const unenrollSpy = sinon.stub(action, "unenroll");
await action.finalize();
Assert.deepEqual(unenrollSpy.args, [[study.recipeId, "recipe-not-seen"]], "unenroll should be called");
},
);