diff options
Diffstat (limited to 'toolkit/components/normandy/test/browser/browser_BaseAction.js')
-rw-r--r-- | toolkit/components/normandy/test/browser/browser_BaseAction.js | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/toolkit/components/normandy/test/browser/browser_BaseAction.js b/toolkit/components/normandy/test/browser/browser_BaseAction.js new file mode 100644 index 0000000000..240a235346 --- /dev/null +++ b/toolkit/components/normandy/test/browser/browser_BaseAction.js @@ -0,0 +1,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"); +}); |