summaryrefslogtreecommitdiffstats
path: root/toolkit/components/normandy/test/browser/browser_BaseAction.js
blob: 240a23534608a39ab9a4e0a7cceed846623496ba (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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
"use strict";

const { BaseAction } = ChromeUtils.importESModule(
  "resource://normandy/actions/BaseAction.sys.mjs"
);
const { Uptake } = ChromeUtils.importESModule(
  "resource://normandy/lib/Uptake.sys.mjs"
);

class NoopAction extends BaseAction {
  constructor() {
    super();
    this._testPreExecutionFlag = false;
    this._testRunFlag = false;
    this._testFinalizeFlag = false;
  }

  _preExecution() {
    this._testPreExecutionFlag = true;
  }

  _run(recipe) {
    this._testRunFlag = true;
  }

  _finalize() {
    this._testFinalizeFlag = true;
  }
}

NoopAction._errorToThrow = new Error("test error");

class FailPreExecutionAction extends NoopAction {
  _preExecution() {
    throw NoopAction._errorToThrow;
  }
}

class FailRunAction extends NoopAction {
  _run(recipe) {
    throw NoopAction._errorToThrow;
  }
}

class FailFinalizeAction extends NoopAction {
  _finalize() {
    throw NoopAction._errorToThrow;
  }
}

// Test that constructor and override methods are run
decorate_task(
  withStub(Uptake, "reportRecipe"),
  withStub(Uptake, "reportAction"),
  async () => {
    let action = new NoopAction();
    is(
      action._testPreExecutionFlag,
      false,
      "_preExecution should not have been called on a new action"
    );
    is(
      action._testRunFlag,
      false,
      "_run has should not have been called on a new action"
    );
    is(
      action._testFinalizeFlag,
      false,
      "_finalize should not be called on a new action"
    );

    const recipe = recipeFactory();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(
      action._testPreExecutionFlag,
      true,
      "_preExecution should be called when a recipe is executed"
    );
    is(
      action._testRunFlag,
      true,
      "_run should be called when a recipe is executed"
    );
    is(
      action._testFinalizeFlag,
      false,
      "_finalize should not have been called when a recipe is executed"
    );

    await action.finalize();
    is(
      action._testFinalizeFlag,
      true,
      "_finalizeExecution should be called when finalize was called"
    );

    action = new NoopAction();
    await action.finalize();
    is(
      action._testPreExecutionFlag,
      true,
      "_preExecution should be called when finalized even if no recipes"
    );
    is(
      action._testRunFlag,
      false,
      "_run should be called if no recipes were run"
    );
    is(
      action._testFinalizeFlag,
      true,
      "_finalize should be called when finalized"
    );
  }
);

// Test that per-recipe uptake telemetry is recorded
decorate_task(
  withStub(Uptake, "reportRecipe"),
  async function ({ reportRecipeStub }) {
    const action = new NoopAction();
    const recipe = recipeFactory();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    Assert.deepEqual(
      reportRecipeStub.args,
      [[recipe, Uptake.RECIPE_SUCCESS]],
      "per-recipe uptake telemetry should be reported"
    );
  }
);

// Finalize causes action telemetry to be recorded
decorate_task(
  withStub(Uptake, "reportAction"),
  async function ({ reportActionStub }) {
    const action = new NoopAction();
    await action.finalize();
    Assert.equal(
      action.state,
      NoopAction.STATE_FINALIZED,
      "Action should be marked as finalized"
    );
    Assert.deepEqual(
      reportActionStub.args,
      [[action.name, Uptake.ACTION_SUCCESS]],
      "action uptake telemetry should be reported"
    );
  }
);

// Recipes can't be run after finalize is called
decorate_task(
  withStub(Uptake, "reportRecipe"),
  async function ({ reportRecipeStub }) {
    const action = new NoopAction();
    const recipe1 = recipeFactory();
    const recipe2 = recipeFactory();

    await action.processRecipe(recipe1, BaseAction.suitability.FILTER_MATCH);
    await action.finalize();

    await Assert.rejects(
      action.processRecipe(recipe2, BaseAction.suitability.FILTER_MATCH),
      /^Error: Action has already been finalized$/,
      "running recipes after finalization is an error"
    );

    Assert.deepEqual(
      reportRecipeStub.args,
      [[recipe1, Uptake.RECIPE_SUCCESS]],
      "Only recipes executed prior to finalizer should report uptake telemetry"
    );
  }
);

// Test an action with a failing pre-execution step
decorate_task(
  withStub(Uptake, "reportRecipe"),
  withStub(Uptake, "reportAction"),
  async function ({ reportRecipeStub, reportActionStub }) {
    const recipe = recipeFactory();
    const action = new FailPreExecutionAction();
    is(
      action.state,
      FailPreExecutionAction.STATE_PREPARING,
      "Pre-execution should not happen immediately"
    );

    // Should fail, putting the action into a "failed" state, but the entry
    // point `processRecipe` should not itself throw an exception.
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(
      action.state,
      FailPreExecutionAction.STATE_FAILED,
      "Action fails if pre-execution fails"
    );
    is(
      action.lastError,
      NoopAction._errorToThrow,
      "The thrown error should be stored in lastError"
    );

    // Should not throw, even though the action is in a disabled state.
    await action.finalize();
    is(
      action.state,
      FailPreExecutionAction.STATE_FINALIZED,
      "Action should be finalized"
    );
    is(
      action.lastError,
      NoopAction._errorToThrow,
      "lastError should not have changed"
    );

    is(action._testRunFlag, false, "_run should not have been called");
    is(
      action._testFinalizeFlag,
      false,
      "_finalize should not have been called"
    );

    Assert.deepEqual(
      reportRecipeStub.args,
      [[recipe, Uptake.RECIPE_ACTION_DISABLED]],
      "Recipe should report recipe status as action disabled"
    );

    Assert.deepEqual(
      reportActionStub.args,
      [[action.name, Uptake.ACTION_PRE_EXECUTION_ERROR]],
      "Action should report pre execution error"
    );
  }
);

// Test an action with a failing recipe step
decorate_task(
  withStub(Uptake, "reportRecipe"),
  withStub(Uptake, "reportAction"),
  async function ({ reportRecipeStub, reportActionStub }) {
    const recipe = recipeFactory();
    const action = new FailRunAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    is(
      action.state,
      FailRunAction.STATE_READY,
      "Action should not be marked as failed due to a recipe failure"
    );
    await action.finalize();
    is(
      action.state,
      FailRunAction.STATE_FINALIZED,
      "Action should be marked as finalized after finalize is called"
    );

    ok(action._testFinalizeFlag, "_finalize should have been called");

    Assert.deepEqual(
      reportRecipeStub.args,
      [[recipe, Uptake.RECIPE_EXECUTION_ERROR]],
      "Recipe should report recipe execution error"
    );

    Assert.deepEqual(
      reportActionStub.args,
      [[action.name, Uptake.ACTION_SUCCESS]],
      "Action should report success"
    );
  }
);

// Test an action with a failing finalize step
decorate_task(
  withStub(Uptake, "reportRecipe"),
  withStub(Uptake, "reportAction"),
  async function ({ reportRecipeStub, reportActionStub }) {
    const recipe = recipeFactory();
    const action = new FailFinalizeAction();
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);
    await action.finalize();

    Assert.deepEqual(
      reportRecipeStub.args,
      [[recipe, Uptake.RECIPE_SUCCESS]],
      "Recipe should report success"
    );

    Assert.deepEqual(
      reportActionStub.args,
      [[action.name, Uptake.ACTION_POST_EXECUTION_ERROR]],
      "Action should report post execution error"
    );
  }
);

// Disable disables an action
decorate_task(
  withStub(Uptake, "reportRecipe"),
  withStub(Uptake, "reportAction"),
  async function ({ reportRecipeStub, reportActionStub }) {
    const recipe = recipeFactory();
    const action = new NoopAction();

    action.disable();
    is(
      action.state,
      NoopAction.STATE_DISABLED,
      "Action should be marked as disabled"
    );

    // Should not throw, even though the action is disabled
    await action.processRecipe(recipe, BaseAction.suitability.FILTER_MATCH);

    // Should not throw, even though the action is disabled
    await action.finalize();

    is(action._testRunFlag, false, "_run should not have been called");
    is(
      action._testFinalizeFlag,
      false,
      "_finalize should not have been called"
    );

    Assert.deepEqual(
      reportActionStub.args,
      [[action.name, Uptake.ACTION_SUCCESS]],
      "Action should not report pre execution error"
    );

    Assert.deepEqual(
      reportRecipeStub.args,
      [[recipe, Uptake.RECIPE_ACTION_DISABLED]],
      "Recipe should report recipe status as action disabled"
    );
  }
);

// If the capabilities don't match, processRecipe shouldn't validate the arguments
decorate_task(async function () {
  const recipe = recipeFactory();
  const action = new NoopAction();
  const verifySpy = sinon.spy(action, "validateArguments");
  await action.processRecipe(
    recipe,
    BaseAction.suitability.CAPABILITIES_MISMATCH
  );
  ok(!verifySpy.called, "validateArguments should not be called");
});