forked from mirrors/gecko-dev
* 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
313 lines
10 KiB
JavaScript
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();
|
|
},
|
|
);
|