fune/toolkit/components/normandy/test/browser/browser_ActionsManager.js
Mike Cooper 011527f7ec Bug 1440777 - Add support for local actions and implement console-log as a local action r=Gijs
* Add ActionsManager to provide a common interface to local and remote actions
* Move action handle logic from RecipeRunner to new ActionsManager
* Implement BaseAction for all local actions
* Implement ConsoleLog as a subclass of BaseAction
* Validate action arguments with schema validator from PolicyEngine

MozReview-Commit-ID: E2cxkkvYjCz

--HG--
extra : rebase_source : 8e68674a08011208dad0f763fe1867df6808d837
2018-03-15 13:14:56 -07:00

313 lines
10 KiB
JavaScript

"use strict";
ChromeUtils.import("resource://normandy/lib/ActionsManager.jsm", this);
ChromeUtils.import("resource://normandy/lib/NormandyApi.jsm", this);
ChromeUtils.import("resource://normandy/lib/Uptake.jsm", this);
// It should only fetch implementations for actions that don't exist locally
decorate_task(
withStub(NormandyApi, "fetchActions"),
withStub(NormandyApi, "fetchImplementation"),
async function(fetchActionsStub, fetchImplementationStub) {
const remoteAction = {name: "remote-action"};
const localAction = {name: "local-action"};
fetchActionsStub.resolves([remoteAction, localAction]);
fetchImplementationStub.callsFake(async () => "");
const manager = new ActionsManager();
manager.localActions = {"local-action": {}};
await manager.fetchRemoteActions();
is(fetchActionsStub.callCount, 1, "action metadata should be fetched");
Assert.deepEqual(
fetchImplementationStub.getCall(0).args,
[remoteAction],
"only the remote action's implementation should be fetched",
);
},
);
// Test life cycle methods for remote actions
decorate_task(
withStub(Uptake, "reportAction"),
withStub(Uptake, "reportRecipe"),
async function(reportActionStub, reportRecipeStub) {
let manager = new ActionsManager();
const recipe = {id: 1, action: "test-remote-action-used"};
const sandboxManagerUsed = {
removeHold: sinon.stub(),
runAsyncCallback: sinon.stub(),
};
const sandboxManagerUnused = {
removeHold: sinon.stub(),
runAsyncCallback: sinon.stub(),
};
manager.remoteActionSandboxes = {
"test-remote-action-used": sandboxManagerUsed,
"test-remote-action-unused": sandboxManagerUnused
};
manager.localActions = {};
await manager.preExecution();
await manager.runRecipe(recipe);
await manager.finalize();
Assert.deepEqual(
sandboxManagerUsed.runAsyncCallback.args,
[
["preExecution"],
["action", recipe],
["postExecution"],
],
"The expected life cycle events should be called on the used sandbox action manager",
);
Assert.deepEqual(
sandboxManagerUnused.runAsyncCallback.args,
[
["preExecution"],
["postExecution"],
],
"The expected life cycle events should be called on the unused sandbox action manager",
);
Assert.deepEqual(
sandboxManagerUsed.removeHold.args,
[["ActionsManager"]],
"ActionsManager should remove holds on the sandbox managers during finalize.",
);
Assert.deepEqual(
sandboxManagerUnused.removeHold.args,
[["ActionsManager"]],
"ActionsManager should remove holds on the sandbox managers during finalize.",
);
Assert.deepEqual(reportActionStub.args, [
["test-remote-action-used", Uptake.ACTION_SUCCESS],
["test-remote-action-unused", Uptake.ACTION_SUCCESS],
]);
Assert.deepEqual(reportRecipeStub.args, [[recipe.id, Uptake.RECIPE_SUCCESS]]);
},
);
// Test life cycle for remote action that fails in pre-step
decorate_task(
withStub(Uptake, "reportAction"),
withStub(Uptake, "reportRecipe"),
async function(reportActionStub, reportRecipeStub) {
let manager = new ActionsManager();
const recipe = {id: 1, action: "test-remote-action-broken"};
const sandboxManagerBroken = {
removeHold: sinon.stub(),
runAsyncCallback: sinon.stub().callsFake(callbackName => {
if (callbackName === "preExecution") {
throw new Error("mock preExecution failure");
}
}),
};
manager.remoteActionSandboxes = {
"test-remote-action-broken": sandboxManagerBroken,
};
manager.localActions = {};
await manager.preExecution();
await manager.runRecipe(recipe);
await manager.finalize();
Assert.deepEqual(
sandboxManagerBroken.runAsyncCallback.args,
[["preExecution"]],
"No async callbacks should be called after preExecution fails",
);
Assert.deepEqual(
sandboxManagerBroken.removeHold.args,
[["ActionsManager"]],
"sandbox holds should still be removed after a failure",
);
Assert.deepEqual(reportActionStub.args, [
["test-remote-action-broken", Uptake.ACTION_PRE_EXECUTION_ERROR],
]);
Assert.deepEqual(reportRecipeStub.args, [[recipe.id, Uptake.RECIPE_ACTION_DISABLED]]);
},
);
// Test life cycle for remote action that fails on a recipe-step
decorate_task(
withStub(Uptake, "reportAction"),
withStub(Uptake, "reportRecipe"),
async function(reportActionStub, reportRecipeStub) {
let manager = new ActionsManager();
const recipe = {id: 1, action: "test-remote-action-broken"};
const sandboxManagerBroken = {
removeHold: sinon.stub(),
runAsyncCallback: sinon.stub().callsFake(callbackName => {
if (callbackName === "action") {
throw new Error("mock action failure");
}
}),
};
manager.remoteActionSandboxes = {
"test-remote-action-broken": sandboxManagerBroken,
};
manager.localActions = {};
await manager.preExecution();
await manager.runRecipe(recipe);
await manager.finalize();
Assert.deepEqual(
sandboxManagerBroken.runAsyncCallback.args,
[["preExecution"], ["action", recipe], ["postExecution"]],
"postExecution callback should still be called after action callback fails",
);
Assert.deepEqual(
sandboxManagerBroken.removeHold.args,
[["ActionsManager"]],
"sandbox holds should still be removed after a recipe failure",
);
Assert.deepEqual(reportActionStub.args, [["test-remote-action-broken", Uptake.ACTION_SUCCESS]]);
Assert.deepEqual(reportRecipeStub.args, [[recipe.id, Uptake.RECIPE_EXECUTION_ERROR]]);
},
);
// Test life cycle for remote action that fails in post-step
decorate_task(
withStub(Uptake, "reportAction"),
withStub(Uptake, "reportRecipe"),
async function(reportActionStub, reportRecipeStub) {
let manager = new ActionsManager();
const recipe = {id: 1, action: "test-remote-action-broken"};
const sandboxManagerBroken = {
removeHold: sinon.stub(),
runAsyncCallback: sinon.stub().callsFake(callbackName => {
if (callbackName === "postExecution") {
throw new Error("mock postExecution failure");
}
}),
};
manager.remoteActionSandboxes = {
"test-remote-action-broken": sandboxManagerBroken,
};
manager.localActions = {};
await manager.preExecution();
await manager.runRecipe(recipe);
await manager.finalize();
Assert.deepEqual(
sandboxManagerBroken.runAsyncCallback.args,
[["preExecution"], ["action", recipe], ["postExecution"]],
"All callbacks should be executed",
);
Assert.deepEqual(
sandboxManagerBroken.removeHold.args,
[["ActionsManager"]],
"sandbox holds should still be removed after a failure",
);
Assert.deepEqual(reportRecipeStub.args, [[recipe.id, Uptake.RECIPE_SUCCESS]]);
Assert.deepEqual(reportActionStub.args, [
["test-remote-action-broken", Uptake.ACTION_POST_EXECUTION_ERROR],
]);
},
);
// Test life cycle methods for local actions
decorate_task(
async function(reportActionStub, Stub) {
let manager = new ActionsManager();
const recipe = {id: 1, action: "test-local-action-used"};
let actionUsed = {
runRecipe: sinon.stub(),
finalize: sinon.stub(),
};
let actionUnused = {
runRecipe: sinon.stub(),
finalize: sinon.stub(),
};
manager.localActions = {
"test-local-action-used": actionUsed,
"test-local-action-unused": actionUnused,
};
manager.remoteActionSandboxes = {};
await manager.preExecution();
await manager.runRecipe(recipe);
await manager.finalize();
Assert.deepEqual(actionUsed.runRecipe.args, [[recipe]], "used action should be called with the recipe");
ok(actionUsed.finalize.calledOnce, "finalize should be called on used action");
Assert.deepEqual(actionUnused.runRecipe.args, [], "unused action should not be called with the recipe");
ok(actionUnused.finalize.calledOnce, "finalize should be called on the unused action");
// Uptake telemetry is handled by actions directly, so doesn't
// need to be tested for local action handling here.
},
);
// Likewise, error handling is dealt with internal to actions as well,
// so doesn't need to be tested as a part of ActionsManager.
// Test fetch remote actions
decorate_task(
withStub(NormandyApi, "fetchActions"),
withStub(NormandyApi, "fetchImplementation"),
withStub(Uptake, "reportAction"),
async function(fetchActionsStub, fetchImplementationStub, reportActionStub) {
fetchActionsStub.callsFake(async () => [
{name: "remoteAction"},
{name: "missingImpl"},
{name: "migratedAction"},
]);
fetchImplementationStub.callsFake(async ({ name }) => {
switch (name) {
case "remoteAction":
return "window.scriptRan = true";
case "missingImpl":
throw new Error(`Could not fetch implementation for ${name}: test error`);
case "migratedAction":
return "// this shouldn't be requested";
default:
throw new Error(`Could not fetch implementation for ${name}: unexpected action`);
}
});
const manager = new ActionsManager();
manager.localActions = {
migratedAction: {finalize: sinon.stub()},
};
await manager.fetchRemoteActions();
Assert.deepEqual(
Object.keys(manager.remoteActionSandboxes),
["remoteAction"],
"remote action should have been loaded",
);
Assert.deepEqual(
fetchImplementationStub.args,
[[{name: "remoteAction"}], [{name: "missingImpl"}]],
"all remote actions should be requested",
);
Assert.deepEqual(
reportActionStub.args,
[["missingImpl", Uptake.ACTION_SERVER_ERROR]],
"Missing implementation should be reported via Uptake",
);
ok(
await manager.remoteActionSandboxes.remoteAction.evalInSandbox("window.scriptRan"),
"Implementations should be run in the sandbox",
);
// clean up sandboxes made by fetchRemoteActions
manager.finalize();
},
);