summaryrefslogtreecommitdiffstats
path: root/toolkit/components/normandy/test/browser/browser_AddonStudies.js
blob: e696b86e364a6189028a7fb51e751ae8afdce615 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
"use strict";

const { IndexedDB } = ChromeUtils.importESModule(
  "resource://gre/modules/IndexedDB.sys.mjs"
);
const { AddonManager } = ChromeUtils.import(
  "resource://gre/modules/AddonManager.jsm"
);
const { NormandyTestUtils } = ChromeUtils.import(
  "resource://testing-common/NormandyTestUtils.jsm"
);
const {
  addonStudyFactory,
  branchedAddonStudyFactory,
} = NormandyTestUtils.factories;

// Initialize test utils
AddonTestUtils.initMochitest(this);

decorate_task(AddonStudies.withStudies(), async function testGetMissing() {
  ok(
    !(await AddonStudies.get("does-not-exist")),
    "get returns null when the requested study does not exist"
  );
});

decorate_task(
  AddonStudies.withStudies([addonStudyFactory({ slug: "test-study" })]),
  async function testGet({ addonStudies: [study] }) {
    const storedStudy = await AddonStudies.get(study.recipeId);
    Assert.deepEqual(study, storedStudy, "get retrieved a study from storage.");
  }
);

decorate_task(
  AddonStudies.withStudies([addonStudyFactory(), addonStudyFactory()]),
  async function testGetAll({ addonStudies }) {
    const storedStudies = await AddonStudies.getAll();
    Assert.deepEqual(
      new Set(storedStudies),
      new Set(addonStudies),
      "getAll returns every stored study."
    );
  }
);

decorate_task(
  AddonStudies.withStudies([addonStudyFactory({ slug: "test-study" })]),
  async function testHas({ addonStudies: [study] }) {
    let hasStudy = await AddonStudies.has(study.recipeId);
    ok(hasStudy, "has returns true for a study that exists in storage.");

    hasStudy = await AddonStudies.has("does-not-exist");
    ok(
      !hasStudy,
      "has returns false for a study that doesn't exist in storage."
    );
  }
);

decorate_task(
  AddonStudies.withStudies([
    addonStudyFactory({ slug: "test-study1" }),
    addonStudyFactory({ slug: "test-study2" }),
  ]),
  async function testClear({ addonStudies: [study1, study2] }) {
    const hasAll =
      (await AddonStudies.has(study1.recipeId)) &&
      (await AddonStudies.has(study2.recipeId));
    ok(hasAll, "Before calling clear, both studies are in storage.");

    await AddonStudies.clear();
    const hasAny =
      (await AddonStudies.has(study1.recipeId)) ||
      (await AddonStudies.has(study2.recipeId));
    ok(!hasAny, "After calling clear, all studies are removed from storage.");
  }
);

decorate_task(
  AddonStudies.withStudies([addonStudyFactory({ slug: "foo" })]),
  async function testUpdate({ addonStudies: [study] }) {
    Assert.deepEqual(await AddonStudies.get(study.recipeId), study);

    const updatedStudy = {
      ...study,
      slug: "bar",
    };
    await AddonStudies.update(updatedStudy);

    Assert.deepEqual(await AddonStudies.get(study.recipeId), updatedStudy);
  }
);

decorate_task(
  AddonStudies.withStudies([
    addonStudyFactory({
      active: true,
      addonId: "does.not.exist@example.com",
      studyEndDate: null,
    }),
    addonStudyFactory({ active: true, addonId: "installed@example.com" }),
    addonStudyFactory({
      active: false,
      addonId: "already.gone@example.com",
      studyEndDate: new Date(2012, 1),
    }),
  ]),
  withSendEventSpy(),
  withInstalledWebExtension(
    { id: "installed@example.com" },
    { expectUninstall: true }
  ),
  async function testInit({
    addonStudies: [activeUninstalledStudy, activeInstalledStudy, inactiveStudy],
    sendEventSpy,
    installedWebExtension: { addonId },
  }) {
    await AddonStudies.init();

    const newActiveStudy = await AddonStudies.get(
      activeUninstalledStudy.recipeId
    );
    ok(
      !newActiveStudy.active,
      "init marks studies as inactive if their add-on is not installed."
    );
    ok(
      newActiveStudy.studyEndDate,
      "init sets the study end date if a study's add-on is not installed."
    );
    let events = Services.telemetry.snapshotEvents(
      Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
      false
    );
    events = (events.parent || []).filter(e => e[1] == "normandy");
    Assert.deepEqual(
      events[0].slice(2), // strip timestamp and "normandy"
      [
        "unenroll",
        "addon_study",
        activeUninstalledStudy.slug,
        {
          addonId: activeUninstalledStudy.addonId,
          addonVersion: activeUninstalledStudy.addonVersion,
          reason: "uninstalled-sideload",
          branch: AddonStudies.NO_BRANCHES_MARKER,
          enrollmentId: events[0][5].enrollmentId,
        },
      ],
      "AddonStudies.init() should send the correct telemetry event"
    );
    ok(
      NormandyTestUtils.isUuid(events[0][5].enrollmentId),
      "enrollment ID should be a UUID"
    );

    const newInactiveStudy = await AddonStudies.get(inactiveStudy.recipeId);
    is(
      newInactiveStudy.studyEndDate.getFullYear(),
      2012,
      "init does not modify inactive studies."
    );

    const newActiveInstalledStudy = await AddonStudies.get(
      activeInstalledStudy.recipeId
    );
    Assert.deepEqual(
      activeInstalledStudy,
      newActiveInstalledStudy,
      "init does not modify studies whose add-on is still installed."
    );

    // Only activeUninstalledStudy should have generated any events
    ok(sendEventSpy.calledOnce, "no extra events should be generated");

    // Clean up
    const addon = await AddonManager.getAddonByID(addonId);
    await addon.uninstall();
    await TestUtils.topicObserved("shield-study-ended", (subject, message) => {
      return message === `${activeInstalledStudy.recipeId}`;
    });
  }
);

// init should register telemetry experiments
decorate_task(
  AddonStudies.withStudies([
    branchedAddonStudyFactory({
      active: true,
      addonId: "installed1@example.com",
    }),
    branchedAddonStudyFactory({
      active: true,
      addonId: "installed2@example.com",
    }),
  ]),
  withInstalledWebExtensionSafe({ id: "installed1@example.com" }),
  withInstalledWebExtension({ id: "installed2@example.com" }),
  withStub(TelemetryEnvironment, "setExperimentActive"),
  async function testInit({ addonStudies, setExperimentActiveStub }) {
    await AddonStudies.init();
    Assert.deepEqual(
      setExperimentActiveStub.args,
      [
        [
          addonStudies[0].slug,
          addonStudies[0].branch,
          {
            type: "normandy-addonstudy",
            enrollmentId: addonStudies[0].enrollmentId,
          },
        ],
        [
          addonStudies[1].slug,
          addonStudies[1].branch,
          {
            type: "normandy-addonstudy",
            enrollmentId: addonStudies[1].enrollmentId,
          },
        ],
      ],
      "Add-on studies are registered in Telemetry by AddonStudies.init"
    );
  }
);

// Test that AddonStudies.init() ends studies that have been uninstalled
decorate_task(
  AddonStudies.withStudies([
    addonStudyFactory({
      active: true,
      addonId: "installed@example.com",
      studyEndDate: null,
    }),
  ]),
  withInstalledWebExtension(
    { id: "installed@example.com" },
    { expectUninstall: true }
  ),
  async function testInit({
    addonStudies: [study],
    installedWebExtension: { addonId },
  }) {
    const addon = await AddonManager.getAddonByID(addonId);
    await addon.uninstall();
    await TestUtils.topicObserved("shield-study-ended", (subject, message) => {
      return message === `${study.recipeId}`;
    });

    const newStudy = await AddonStudies.get(study.recipeId);
    ok(
      !newStudy.active,
      "Studies are marked as inactive when their add-on is uninstalled."
    );
    ok(
      newStudy.studyEndDate,
      "The study end date is set when the add-on for the study is uninstalled."
    );
  }
);

decorate_task(
  AddonStudies.withStudies([
    NormandyTestUtils.factories.addonStudyFactory({ active: true }),
    NormandyTestUtils.factories.branchedAddonStudyFactory(),
  ]),
  async function testRemoveOldAddonStudies({
    addonStudies: [noBranchStudy, branchedStudy],
  }) {
    // pre check, both studies are active
    const preActiveIds = (await AddonStudies.getAllActive()).map(
      addon => addon.recipeId
    );
    Assert.deepEqual(
      preActiveIds,
      [noBranchStudy.recipeId, branchedStudy.recipeId],
      "Both studies should be active"
    );

    // run the migration
    await AddonStudies.migrations.migration02RemoveOldAddonStudyAction();

    // The unbrached study should end
    const postActiveIds = (await AddonStudies.getAllActive()).map(
      addon => addon.recipeId
    );
    Assert.deepEqual(
      postActiveIds,
      [branchedStudy.recipeId],
      "The unbranched study should end"
    );

    // But both studies should still be present
    const postAllIds = (await AddonStudies.getAll()).map(
      addon => addon.recipeId
    );
    Assert.deepEqual(
      postAllIds,
      [noBranchStudy.recipeId, branchedStudy.recipeId],
      "Both studies should still be present"
    );
  }
);