diff options
Diffstat (limited to 'toolkit/components/normandy/test/browser/browser_BaseAction.js')
-rw-r--r-- | toolkit/components/normandy/test/browser/browser_BaseAction.js | 344 |
1 files changed, 344 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..bfc4725c5b --- /dev/null +++ b/toolkit/components/normandy/test/browser/browser_BaseAction.js @@ -0,0 +1,344 @@ +"use strict"; + +const { BaseAction } = ChromeUtils.import( + "resource://normandy/actions/BaseAction.jsm" +); +const { Uptake } = ChromeUtils.import("resource://normandy/lib/Uptake.jsm"); + +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(); + ok( + 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"); +}); |