Bug 1513646 - Remove Normandy remote-action infrastructure r=Gijs

Differential Revision: https://phabricator.services.mozilla.com/D28227

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Michael Cooper 2019-04-23 13:23:07 +00:00
Родитель 98d200e4b4
Коммит 490749ff91
16 изменённых файлов: 14 добавлений и 1373 удалений

Просмотреть файл

@ -1,75 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {NormandyDriver} = ChromeUtils.import("resource://normandy/lib/NormandyDriver.jsm");
const {SandboxManager} = ChromeUtils.import("resource://normandy/lib/SandboxManager.jsm");
var EXPORTED_SYMBOLS = ["ActionSandboxManager"];
/**
* An extension to SandboxManager that prepares a sandbox for executing
* Normandy actions.
*
* Actions register a set of named callbacks, which this class makes available
* for execution. This allows a single action script to define multiple,
* independent steps that execute in isolated sandboxes.
*
* Callbacks are assumed to be async and must return Promises.
*/
var ActionSandboxManager = class extends SandboxManager {
constructor(actionScript) {
super();
// Prepare the sandbox environment
const driver = new NormandyDriver(this);
this.cloneIntoGlobal("sandboxedDriver", driver, {cloneFunctions: true});
this.evalInSandbox(`
// Shim old API for registering actions
function registerAction(name, Action) {
registerAsyncCallback("action", (driver, recipe) => {
return new Action(driver, recipe).execute();
});
};
this.asyncCallbacks = new Map();
function registerAsyncCallback(name, callback) {
asyncCallbacks.set(name, callback);
}
this.window = this;
this.setTimeout = sandboxedDriver.setTimeout;
this.clearTimeout = sandboxedDriver.clearTimeout;
`);
this.evalInSandbox(actionScript);
}
/**
* Execute a callback in the sandbox with the given name. If the script does
* not register a callback with the given name, we log a message and return.
* @param {String} callbackName
* @param {...*} [args]
* Remaining arguments are cloned into the sandbox and passed as arguments
* to the callback.
* @resolves
* The return value of the callback, cloned into the current compartment, or
* undefined if a matching callback was not found.
* @rejects
* If the sandbox rejects, an error object with the message from the sandbox
* error. Due to sandbox limitations, the stack trace is not preserved.
*/
async runAsyncCallback(callbackName, ...args) {
const callbackWasRegistered = this.evalInSandbox(`asyncCallbacks.has("${callbackName}")`);
if (!callbackWasRegistered) {
return undefined;
}
this.cloneIntoGlobal("callbackArgs", args);
const result = await this.evalInSandbox(`
asyncCallbacks.get("${callbackName}")(sandboxedDriver, ...callbackArgs);
`);
return Cu.cloneInto(result, {});
}
};

Просмотреть файл

@ -2,10 +2,8 @@ const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm")
const {LogManager} = ChromeUtils.import("resource://normandy/lib/LogManager.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
ActionSandboxManager: "resource://normandy/lib/ActionSandboxManager.jsm",
AddonStudyAction: "resource://normandy/actions/AddonStudyAction.jsm",
ConsoleLogAction: "resource://normandy/actions/ConsoleLogAction.jsm",
NormandyApi: "resource://normandy/lib/NormandyApi.jsm",
PreferenceExperimentAction: "resource://normandy/actions/PreferenceExperimentAction.jsm",
PreferenceRollbackAction: "resource://normandy/actions/PreferenceRollbackAction.jsm",
PreferenceRolloutAction: "resource://normandy/actions/PreferenceRolloutAction.jsm",
@ -19,17 +17,10 @@ const log = LogManager.getLogger("recipe-runner");
/**
* A class to manage the actions that recipes can use in Normandy.
*
* This includes both remote and local actions. Remote actions
* implementations are fetched from the Normandy server; their
* lifecycles are managed by `normandy/lib/ActionSandboxManager.jsm`.
* Local actions have their implementations packaged in the Normandy
* client, and manage their lifecycles internally.
*/
class ActionsManager {
constructor() {
this.finalized = false;
this.remoteActionSandboxes = {};
const addonStudyAction = new AddonStudyAction();
@ -44,52 +35,6 @@ class ActionsManager {
};
}
async fetchRemoteActions() {
const actions = await NormandyApi.fetchActions();
for (const action of actions) {
// Skip actions with local implementations
if (action.name in this.localActions) {
continue;
}
try {
const implementation = await NormandyApi.fetchImplementation(action);
const sandbox = new ActionSandboxManager(implementation);
sandbox.addHold("ActionsManager");
this.remoteActionSandboxes[action.name] = sandbox;
} catch (err) {
log.warn(`Could not fetch implementation for ${action.name}: ${err}`);
let status;
if (/NetworkError/.test(err)) {
status = Uptake.ACTION_NETWORK_ERROR;
} else {
status = Uptake.ACTION_SERVER_ERROR;
}
await Uptake.reportAction(action.name, status);
}
}
const actionNames = Object.keys(this.remoteActionSandboxes);
log.debug(`Fetched ${actionNames.length} actions from the server: ${actionNames.join(", ")}`);
}
async preExecution() {
// Local actions run pre-execution hooks implicitly
for (const [actionName, manager] of Object.entries(this.remoteActionSandboxes)) {
try {
await manager.runAsyncCallback("preExecution");
manager.disabled = false;
} catch (err) {
log.error(`Could not run pre-execution hook for ${actionName}:`, err.message);
manager.disabled = true;
await Uptake.reportAction(actionName, Uptake.ACTION_PRE_EXECUTION_ERROR);
}
}
}
async runRecipe(recipe) {
let actionName = recipe.action;
@ -97,27 +42,6 @@ class ActionsManager {
log.info(`Executing recipe "${recipe.name}" (action=${recipe.action})`);
const action = this.localActions[actionName];
await action.runRecipe(recipe);
} else if (actionName in this.remoteActionSandboxes) {
let status;
const manager = this.remoteActionSandboxes[recipe.action];
if (manager.disabled) {
log.warn(
`Skipping recipe ${recipe.name} because ${recipe.action} failed during pre-execution.`
);
status = Uptake.RECIPE_ACTION_DISABLED;
} else {
try {
log.info(`Executing recipe "${recipe.name}" (action=${recipe.action})`);
await manager.runAsyncCallback("action", recipe);
status = Uptake.RECIPE_SUCCESS;
} catch (e) {
e.message = `Could not execute recipe ${recipe.name}: ${e.message}`;
Cu.reportError(e);
status = Uptake.RECIPE_EXECUTION_ERROR;
}
}
await Uptake.reportRecipe(recipe, status);
} else {
log.error(
`Could not execute recipe ${recipe.name}:`,
@ -137,26 +61,5 @@ class ActionsManager {
for (const action of new Set(Object.values(this.localActions))) {
action.finalize();
}
// Run post-execution hooks for remote actions
for (const [actionName, manager] of Object.entries(this.remoteActionSandboxes)) {
// Skip if pre-execution failed.
if (manager.disabled) {
log.info(`Skipping post-execution hook for ${actionName} due to earlier failure.`);
continue;
}
try {
await manager.runAsyncCallback("postExecution");
await Uptake.reportAction(actionName, Uptake.ACTION_SUCCESS);
} catch (err) {
log.info(`Could not run post-execution hook for ${actionName}:`, err.message);
await Uptake.reportAction(actionName, Uptake.ACTION_POST_EXECUTION_ERROR);
}
}
// Nuke sandboxes
Object.values(this.remoteActionSandboxes)
.forEach(manager => manager.removeHold("ActionsManager"));
}
}

Просмотреть файл

@ -1,193 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
const {ShellService} = ChromeUtils.import("resource:///modules/ShellService.jsm");
const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
const {clearTimeout, setTimeout} = ChromeUtils.import("resource://gre/modules/Timer.jsm");
const {LogManager} = ChromeUtils.import("resource://normandy/lib/LogManager.jsm");
const {Storage} = ChromeUtils.import("resource://normandy/lib/Storage.jsm");
const {ClientEnvironment} = ChromeUtils.import("resource://normandy/lib/ClientEnvironment.jsm");
const {PreferenceExperiments} = ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm");
ChromeUtils.defineModuleGetter(
this, "Sampling", "resource://gre/modules/components-utils/Sampling.jsm");
ChromeUtils.defineModuleGetter(this, "UpdateUtils", "resource://gre/modules/UpdateUtils.jsm");
const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
var EXPORTED_SYMBOLS = ["NormandyDriver"];
const actionLog = LogManager.getLogger("normandy-driver.actions");
var NormandyDriver = function(sandboxManager) {
if (!sandboxManager) {
throw new Error("sandboxManager is required");
}
const {sandbox} = sandboxManager;
return {
testing: false,
get locale() {
if (Services.locale.getAppLocaleAsLangTag) {
return Services.locale.getAppLocaleAsLangTag;
}
return Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry)
.getSelectedLocale("global");
},
get userId() {
return ClientEnvironment.userId;
},
log(message, level = "debug") {
const levels = ["debug", "info", "warn", "error"];
if (!levels.includes(level)) {
throw new Error(`Invalid log level "${level}"`);
}
actionLog[level](message);
},
client() {
const appinfo = {
version: Services.appinfo.version,
channel: UpdateUtils.getUpdateChannel(false),
isDefaultBrowser: ShellService.isDefaultBrowser() || null,
searchEngine: null,
syncSetup: Preferences.isSet("services.sync.username"),
syncDesktopDevices: Preferences.get("services.sync.clients.devices.desktop", 0),
syncMobileDevices: Preferences.get("services.sync.clients.devices.mobile", 0),
syncTotalDevices: null,
plugins: {},
doNotTrack: Preferences.get("privacy.donottrackheader.enabled", false),
distribution: Preferences.get("distribution.id", "default"),
};
appinfo.syncTotalDevices = appinfo.syncDesktopDevices + appinfo.syncMobileDevices;
const searchEnginePromise = new Promise(resolve => {
Services.search.init().then(() => {
appinfo.searchEngine = Services.search.defaultEngine.identifier;
}).finally(resolve);
});
const pluginsPromise = (async () => {
let plugins = await AddonManager.getAddonsByTypes(["plugin"]);
plugins.forEach(plugin => appinfo.plugins[plugin.name] = {
name: plugin.name,
description: plugin.description,
version: plugin.version,
});
})();
return new sandbox.Promise(resolve => {
Promise.all([searchEnginePromise, pluginsPromise]).then(() => {
resolve(Cu.cloneInto(appinfo, sandbox));
});
});
},
uuid() {
let ret = generateUUID().toString();
ret = ret.slice(1, ret.length - 1);
return ret;
},
createStorage(prefix) {
const storage = new Storage(prefix);
// Wrapped methods that we expose to the sandbox. These are documented in
// the driver spec in docs/dev/driver.rst.
const storageInterface = {};
for (const method of ["getItem", "setItem", "removeItem", "clear"]) {
storageInterface[method] = sandboxManager.wrapAsync(storage[method].bind(storage), {
cloneArguments: true,
cloneInto: true,
});
}
return sandboxManager.cloneInto(storageInterface, {cloneFunctions: true});
},
setTimeout(cb, time) {
if (typeof cb !== "function") {
throw new sandbox.Error(`setTimeout must be called with a function, got "${typeof cb}"`);
}
const token = setTimeout(() => {
cb();
sandboxManager.removeHold(`setTimeout-${token}`);
}, time);
sandboxManager.addHold(`setTimeout-${token}`);
return Cu.cloneInto(token, sandbox);
},
clearTimeout(token) {
clearTimeout(token);
sandboxManager.removeHold(`setTimeout-${token}`);
},
// Sampling
ratioSample: sandboxManager.wrapAsync(Sampling.ratioSample),
// Preference Experiment API
preferenceExperiments: {
start: sandboxManager.wrapAsync(
PreferenceExperiments.start.bind(PreferenceExperiments),
{cloneArguments: true}
),
markLastSeen: sandboxManager.wrapAsync(
PreferenceExperiments.markLastSeen.bind(PreferenceExperiments)
),
stop: sandboxManager.wrapAsync(PreferenceExperiments.stop.bind(PreferenceExperiments)),
get: sandboxManager.wrapAsync(
PreferenceExperiments.get.bind(PreferenceExperiments),
{cloneInto: true}
),
getAllActive: sandboxManager.wrapAsync(
PreferenceExperiments.getAllActive.bind(PreferenceExperiments),
{cloneInto: true}
),
has: sandboxManager.wrapAsync(PreferenceExperiments.has.bind(PreferenceExperiments)),
},
// Preference read-only API
preferences: {
getBool: wrapPrefGetter(Services.prefs.getBoolPref),
getInt: wrapPrefGetter(Services.prefs.getIntPref),
getChar: wrapPrefGetter(Services.prefs.getCharPref),
has(name) {
return Services.prefs.getPrefType(name) !== Services.prefs.PREF_INVALID;
},
},
};
};
/**
* Wrap a getter form nsIPrefBranch for use in the sandbox.
*
* We don't want to export the getters directly in case they add parameters that
* aren't safe for the sandbox without us noticing; wrapping helps prevent
* passing unknown parameters.
*
* @param {Function} getter
* Function on an nsIPrefBranch that fetches a preference value.
* @return {Function}
*/
function wrapPrefGetter(getter) {
return (value, defaultValue = undefined) => {
// Passing undefined as the defaultValue disables throwing exceptions when
// the pref is missing or the type doesn't match, so we need to specifically
// exclude it if we don't want default value behavior.
const args = [value];
if (defaultValue !== undefined) {
args.push(defaultValue);
}
return getter.apply(null, args);
};
}

Просмотреть файл

@ -228,8 +228,6 @@ var RecipeRunner = {
}
const actions = new ActionsManager();
await actions.fetchRemoteActions();
await actions.preExecution();
// Execute recipes, if we have any.
if (recipesToRun.length === 0) {

Просмотреть файл

@ -1,118 +0,0 @@
var EXPORTED_SYMBOLS = ["SandboxManager"];
/**
* A wrapper class with helper methods for manipulating a sandbox.
*
* Along with convenient utility methods, SandboxManagers maintain a list of
* "holds", which prevent the sandbox from being nuked until all registered
* holds are removed. This allows sandboxes to trigger async operations and
* automatically nuke themselves when they're done.
*/
var SandboxManager = class {
constructor() {
this._sandbox = new Cu.Sandbox(null, {
wantComponents: false,
wantGlobalProperties: ["URL", "URLSearchParams"],
});
this.holds = [];
}
get sandbox() {
if (this._sandbox) {
return this._sandbox;
}
throw new Error("Tried to use sandbox after it was nuked");
}
addHold(name) {
this.holds.push(name);
}
removeHold(name) {
const index = this.holds.indexOf(name);
if (index === -1) {
throw new Error(`Tried to remove non-existant hold "${name}"`);
}
this.holds.splice(index, 1);
this.tryCleanup();
}
cloneInto(value, options = {}) {
return Cu.cloneInto(value, this.sandbox, options);
}
cloneIntoGlobal(name, value, options = {}) {
const clonedValue = Cu.cloneInto(value, this.sandbox, options);
this.addGlobal(name, clonedValue);
return clonedValue;
}
addGlobal(name, value) {
this.sandbox[name] = value;
}
evalInSandbox(script) {
return Cu.evalInSandbox(script, this.sandbox);
}
tryCleanup() {
if (this.holds.length === 0) {
const sandbox = this._sandbox;
this._sandbox = null;
Cu.nukeSandbox(sandbox);
}
}
isNuked() {
// Do this in a promise, so other async things can resolve.
return new Promise((resolve, reject) => {
if (!this._sandbox) {
resolve();
} else {
reject(new Error(`Sandbox is not nuked. Holds left: ${this.holds}`));
}
});
}
/**
* Wraps a function that returns a Promise from a privileged (i.e. chrome)
* context and returns a Promise from this SandboxManager's sandbox. Useful
* for exposing privileged functions to the sandbox, since the sandbox can't
* access properties on privileged objects, e.g. Promise.then on a privileged
* Promise.
* @param {Function} wrappedFunction
* @param {Object} [options]
* @param {boolean} [options.cloneInto=false]
* If true, the value resolved by the privileged Promise is cloned into the
* sandbox before being resolved by the sandbox Promise. Without this, the
* result will be Xray-wrapped.
* @param {boolean} [options.cloneArguments=false]
* If true, the arguments passed to wrappedFunction will be cloned into the
* privileged chrome context. If wrappedFunction holds a reference to any of
* its arguments, you will need this to avoid losing access to the arguments
* when the sandbox they originate from is nuked.
* @return {Function}
*/
wrapAsync(wrappedFunction, options = {cloneInto: false, cloneArguments: false}) {
// In order for `this` to work in wrapped functions, we must return a
// non-arrow function, which requires saving a reference to the manager.
const sandboxManager = this;
return function(...args) {
return new sandboxManager.sandbox.Promise((resolve, reject) => {
if (options.cloneArguments) {
args = Cu.cloneInto(args, {});
}
wrappedFunction.apply(this, args).then(result => {
if (options.cloneInto) {
result = sandboxManager.cloneInto(result);
}
resolve(result);
}, err => {
reject(new sandboxManager.sandbox.Error(err.message, err.fileName, err.lineNumber));
});
});
};
}
};

Просмотреть файл

@ -18,7 +18,6 @@ skip-if = !healthreport || !telemetry
[browser_actions_PreferenceRolloutAction.js]
[browser_actions_PreferenceRollbackAction.js]
[browser_actions_ShowHeartbeatAction.js]
[browser_ActionSandboxManager.js]
[browser_ActionsManager.js]
[browser_AddonStudies.js]
skip-if = (verify && (os == 'linux'))
@ -29,7 +28,6 @@ skip-if = (verify && (os == 'linux'))
[browser_Heartbeat.js]
[browser_LogManager.js]
[browser_Normandy.js]
[browser_NormandyDriver.js]
[browser_PreferenceExperiments.js]
[browser_PreferenceRollouts.js]
[browser_RecipeRunner.js]

Просмотреть файл

@ -1,167 +0,0 @@
"use strict";
ChromeUtils.import("resource://normandy/lib/ActionSandboxManager.jsm", this);
ChromeUtils.import("resource://normandy/lib/NormandyDriver.jsm", this);
async function withManager(script, testFunction) {
const manager = new ActionSandboxManager(script);
manager.addHold("testing");
await testFunction(manager);
manager.removeHold("testing");
}
add_task(async function testMissingCallbackName() {
await withManager("1 + 1", async manager => {
is(
await manager.runAsyncCallback("missingCallback"),
undefined,
"runAsyncCallback returns undefined when given a missing callback name",
);
});
});
add_task(async function testCallback() {
const script = `
registerAsyncCallback("testCallback", async function(normandy) {
return 5;
});
`;
await withManager(script, async manager => {
const result = await manager.runAsyncCallback("testCallback");
is(result, 5, "runAsyncCallback executes the named callback inside the sandbox");
});
});
add_task(async function testArguments() {
const script = `
registerAsyncCallback("testCallback", async function(normandy, a, b) {
return a + b;
});
`;
await withManager(script, async manager => {
const result = await manager.runAsyncCallback("testCallback", 4, 6);
is(result, 10, "runAsyncCallback passes arguments to the callback");
});
});
add_task(async function testCloning() {
const script = `
registerAsyncCallback("testCallback", async function(normandy, obj) {
return {foo: "bar", baz: obj.baz};
});
`;
await withManager(script, async manager => {
const result = await manager.runAsyncCallback("testCallback", {baz: "biff"});
Assert.deepEqual(
result,
{foo: "bar", baz: "biff"},
(
"runAsyncCallback clones arguments into the sandbox and return values into the " +
"context it was called from"
),
);
});
});
add_task(async function testError() {
const script = `
registerAsyncCallback("testCallback", async function(normandy) {
throw new Error("WHY")
});
`;
await withManager(script, async manager => {
try {
await manager.runAsyncCallback("testCallback");
ok(false, "runAsnycCallbackFromScript throws errors when raised by the sandbox");
} catch (err) {
is(err.message, "WHY", "runAsnycCallbackFromScript throws errors when raised by the sandbox");
}
});
});
add_task(async function testDriver() {
// The value returned by runAsyncCallback is cloned without the cloneFunctions
// option, so we can't inspect the driver itself since its methods will not be
// present. Instead, we inspect the properties on it available to the sandbox.
const script = `
registerAsyncCallback("testCallback", async function(normandy) {
return Object.keys(normandy);
});
`;
await withManager(script, async manager => {
const sandboxDriverKeys = await manager.runAsyncCallback("testCallback");
const referenceDriver = new NormandyDriver(manager);
for (const prop of Object.keys(referenceDriver)) {
ok(sandboxDriverKeys.includes(prop), `runAsyncCallback's driver has the "${prop}" property.`);
}
});
});
add_task(async function testGlobalObject() {
// Test that window is an alias for the global object, and that it
// has some expected functions available on it.
const script = `
window.setOnWindow = "set";
this.setOnGlobal = "set";
registerAsyncCallback("testCallback", async function(normandy) {
return {
setOnWindow: setOnWindow,
setOnGlobal: window.setOnGlobal,
setTimeoutExists: setTimeout !== undefined,
clearTimeoutExists: clearTimeout !== undefined,
};
});
`;
await withManager(script, async manager => {
const result = await manager.runAsyncCallback("testCallback");
Assert.deepEqual(result, {
setOnWindow: "set",
setOnGlobal: "set",
setTimeoutExists: true,
clearTimeoutExists: true,
}, "sandbox.window is the global object and has expected functions.");
});
});
add_task(async function testRegisterActionShim() {
const recipe = {
foo: "bar",
};
const script = `
class TestAction {
constructor(driver, recipe) {
this.driver = driver;
this.recipe = recipe;
}
execute() {
return new Promise(resolve => {
resolve({
foo: this.recipe.foo,
isDriver: "log" in this.driver,
});
});
}
}
registerAction('test-action', TestAction);
`;
await withManager(script, async manager => {
const result = await manager.runAsyncCallback("action", recipe);
is(result.foo, "bar", "registerAction registers an async callback for actions");
is(
result.isDriver,
true,
"registerAction passes the driver to the action class constructor",
);
});
});

Просмотреть файл

@ -4,219 +4,7 @@ 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, 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, 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, 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, Uptake.RECIPE_SUCCESS]]);
Assert.deepEqual(reportActionStub.args, [
["test-remote-action-broken", Uptake.ACTION_POST_EXECUTION_ERROR],
]);
},
);
// Test life cycle methods for local actions
// Test life cycle methods for actions
decorate_task(
async function(reportActionStub, Stub) {
let manager = new ActionsManager();
@ -234,9 +22,7 @@ decorate_task(
"test-local-action-used": actionUsed,
"test-local-action-unused": actionUnused,
};
manager.remoteActionSandboxes = {};
await manager.preExecution();
await manager.runRecipe(recipe);
await manager.finalize();
@ -244,70 +30,6 @@ decorate_task(
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();
},
);

Просмотреть файл

@ -102,28 +102,6 @@ add_task(async function testExperiments() {
getAll.restore();
});
add_task(withDriver(Assert, async function testAddonsInContext(driver) {
// Create before install so that the listener is added before startup completes.
const startupPromise = AddonTestUtils.promiseWebExtensionStartup("normandydriver@example.com");
const addonInstall = await AddonManager.getInstallForURL(TEST_XPI_URL);
await addonInstall.install();
const addonId = addonInstall.addon.id;
await startupPromise;
const addons = await ClientEnvironment.addons;
Assert.deepEqual(addons[addonId], {
id: [addonId],
name: "normandy_fixture",
version: "1.0",
installDate: addons[addonId].installDate,
isActive: true,
type: "extension",
}, "addons should be available in context");
const addon = await AddonManager.getAddonByID(addonId);
await addon.uninstall();
}));
add_task(async function isFirstRun() {
await SpecialPowers.pushPrefEnv({set: [["app.normandy.first_run", true]]});
ok(ClientEnvironment.isFirstRun, "isFirstRun is read from preferences");

Просмотреть файл

@ -2,7 +2,6 @@
ChromeUtils.import("resource://gre/modules/Services.jsm", this);
ChromeUtils.import("resource://normandy/lib/Heartbeat.jsm", this);
ChromeUtils.import("resource://normandy/lib/SandboxManager.jsm", this);
/**
* Assert an array is in non-descending order, and that every element is a number
@ -68,11 +67,6 @@ function assertTelemetrySent(hb, eventNames) {
});
}
const sandboxManager = new SandboxManager();
sandboxManager.addHold("test running");
// Several of the behaviors of heartbeat prompt are mutually exclusive, so checks are broken up
// into three batches.
@ -181,13 +175,3 @@ add_task(async function() {
await BrowserTestUtils.closeWindow(targetWindow);
await telemetrySentPromise;
});
// Cleanup
add_task(async function() {
// Make sure the sandbox is clean.
sandboxManager.removeHold("test running");
await sandboxManager.isNuked()
.then(() => ok(true, "sandbox is nuked"))
.catch(e => ok(false, "sandbox is nuked", e));
});

Просмотреть файл

@ -1,215 +0,0 @@
"use strict";
ChromeUtils.import("resource://normandy/lib/NormandyDriver.jsm", this);
ChromeUtils.import("resource://normandy/lib/PreferenceExperiments.jsm", this);
add_task(withDriver(Assert, async function uuids(driver) {
// Test that it is a UUID
const uuid1 = driver.uuid();
ok(UUID_REGEX.test(uuid1), "valid uuid format");
// Test that UUIDs are different each time
const uuid2 = driver.uuid();
isnot(uuid1, uuid2, "uuids are unique");
}));
add_task(withDriver(Assert, async function userId(driver) {
// Test that userId is a UUID
ok(UUID_REGEX.test(driver.userId), "userId is a uuid");
}));
add_task(withDriver(Assert, async function syncDeviceCounts(driver) {
let client = await driver.client();
is(client.syncMobileDevices, 0, "syncMobileDevices defaults to zero");
is(client.syncDesktopDevices, 0, "syncDesktopDevices defaults to zero");
is(client.syncTotalDevices, 0, "syncTotalDevices defaults to zero");
await SpecialPowers.pushPrefEnv({
set: [
["services.sync.clients.devices.mobile", 5],
["services.sync.clients.devices.desktop", 4],
],
});
client = await driver.client();
is(client.syncMobileDevices, 5, "syncMobileDevices is read when set");
is(client.syncDesktopDevices, 4, "syncDesktopDevices is read when set");
is(client.syncTotalDevices, 9, "syncTotalDevices is read when set");
}));
add_task(withDriver(Assert, async function distribution(driver) {
let client = await driver.client();
is(client.distribution, "default", "distribution has a default value");
await SpecialPowers.pushPrefEnv({set: [["distribution.id", "funnelcake"]]});
client = await driver.client();
is(client.distribution, "funnelcake", "distribution is read from preferences");
}));
decorate_task(
withSandboxManager(Assert),
async function testCreateStorage(sandboxManager) {
const driver = new NormandyDriver(sandboxManager);
sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true});
// Assertion helpers
sandboxManager.addGlobal("is", is);
sandboxManager.addGlobal("deepEqual", (...args) => Assert.deepEqual(...args));
await sandboxManager.evalInSandbox(`
(async function sandboxTest() {
const store = driver.createStorage("testprefix");
const otherStore = driver.createStorage("othertestprefix");
await store.clear();
await otherStore.clear();
await store.setItem("willremove", 7);
await otherStore.setItem("willremove", 4);
is(await store.getItem("willremove"), 7, "createStorage stores sandbox values");
is(
await otherStore.getItem("willremove"),
4,
"values are not shared between createStorage stores",
);
const deepValue = {"foo": ["bar", "baz"]};
await store.setItem("deepValue", deepValue);
deepEqual(await store.getItem("deepValue"), deepValue, "createStorage clones stored values");
await store.removeItem("willremove");
is(await store.getItem("willremove"), null, "createStorage removes items");
is('prefix' in store, false, "createStorage doesn't expose non-whitelist attributes");
})();
`);
}
);
decorate_task(
withPrefEnv({
set: [
["test.char", "a string"],
["test.int", 5],
["test.bool", true],
],
}),
withSandboxManager(Assert, async function testPreferences(sandboxManager) {
const driver = new NormandyDriver(sandboxManager);
sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true});
// Assertion helpers
sandboxManager.addGlobal("is", is);
sandboxManager.addGlobal("ok", ok);
sandboxManager.addGlobal("assertThrows", Assert.throws.bind(Assert));
await sandboxManager.evalInSandbox(`
(async function sandboxTest() {
ok(
driver.preferences.getBool("test.bool"),
"preferences.getBool can retrieve boolean preferences."
);
is(
driver.preferences.getInt("test.int"),
5,
"preferences.getInt can retrieve integer preferences."
);
is(
driver.preferences.getChar("test.char"),
"a string",
"preferences.getChar can retrieve string preferences."
);
assertThrows(
() => driver.preferences.getChar("test.int"),
"preferences.getChar throws when retreiving a non-string preference."
);
assertThrows(
() => driver.preferences.getInt("test.bool"),
"preferences.getInt throws when retreiving a non-integer preference."
);
assertThrows(
() => driver.preferences.getBool("test.char"),
"preferences.getBool throws when retreiving a non-boolean preference."
);
assertThrows(
() => driver.preferences.getChar("test.does.not.exist"),
"preferences.getChar throws when retreiving a non-existant preference."
);
assertThrows(
() => driver.preferences.getInt("test.does.not.exist"),
"preferences.getInt throws when retreiving a non-existant preference."
);
assertThrows(
() => driver.preferences.getBool("test.does.not.exist"),
"preferences.getBool throws when retreiving a non-existant preference."
);
ok(
driver.preferences.getBool("test.does.not.exist", true),
"preferences.getBool returns a default value if the preference doesn't exist."
);
is(
driver.preferences.getInt("test.does.not.exist", 7),
7,
"preferences.getInt returns a default value if the preference doesn't exist."
);
is(
driver.preferences.getChar("test.does.not.exist", "default"),
"default",
"preferences.getChar returns a default value if the preference doesn't exist."
);
ok(
driver.preferences.has("test.char"),
"preferences.has returns true if the given preference exists."
);
ok(
!driver.preferences.has("test.does.not.exist"),
"preferences.has returns false if the given preference does not exist."
);
})();
`);
})
);
decorate_task(
withSandboxManager(Assert),
withMockPreferences,
PreferenceExperiments.withMockExperiments(),
async function testPreferenceStudies(sandboxManager) {
const driver = new NormandyDriver(sandboxManager);
sandboxManager.cloneIntoGlobal("driver", driver, {cloneFunctions: true});
// Assertion helpers
sandboxManager.addGlobal("is", is);
sandboxManager.addGlobal("ok", ok);
await sandboxManager.evalInSandbox(`
(async function sandboxTest() {
const studyName = "preftest";
let hasStudy = await driver.preferenceExperiments.has(studyName);
ok(!hasStudy, "preferenceExperiments.has returns false if the study hasn't been started yet.");
await driver.preferenceExperiments.start({
name: studyName,
branch: "control",
preferenceName: "test.pref",
preferenceValue: true,
preferenceBranchType: "user",
preferenceType: "boolean",
});
hasStudy = await driver.preferenceExperiments.has(studyName);
ok(hasStudy, "preferenceExperiments.has returns true after the study has been started.");
let study = await driver.preferenceExperiments.get(studyName);
is(
study.branch,
"control",
"preferenceExperiments.get fetches studies from within a sandbox."
);
ok(!study.expired, "Studies are marked as active after being started by the driver.");
await driver.preferenceExperiments.stop(studyName);
study = await driver.preferenceExperiments.get(studyName);
ok(study.expired, "Studies are marked as inactive after being stopped by the driver.");
})();
`);
}
);

Просмотреть файл

@ -6,7 +6,6 @@ ChromeUtils.import("resource://normandy/lib/RecipeRunner.jsm", this);
ChromeUtils.import("resource://normandy/lib/ClientEnvironment.jsm", this);
ChromeUtils.import("resource://normandy/lib/CleanupManager.jsm", this);
ChromeUtils.import("resource://normandy/lib/NormandyApi.jsm", this);
ChromeUtils.import("resource://normandy/lib/ActionSandboxManager.jsm", this);
ChromeUtils.import("resource://normandy/lib/ActionsManager.jsm", this);
ChromeUtils.import("resource://normandy/lib/AddonStudies.jsm", this);
ChromeUtils.import("resource://normandy/lib/Uptake.jsm", this);
@ -128,39 +127,15 @@ decorate_task(
}
);
/**
* Mocks RecipeRunner.loadActionSandboxManagers for testing run.
*/
async function withMockActionSandboxManagers(actions, testFunction) {
const managers = {};
for (const action of actions) {
const manager = new ActionSandboxManager("");
manager.addHold("testing");
managers[action.name] = manager;
sinon.stub(managers[action.name], "runAsyncCallback");
}
await testFunction(managers);
for (const manager of Object.values(managers)) {
manager.removeHold("testing");
await manager.isNuked();
}
}
decorate_task(
withStub(Uptake, "reportRunner"),
withStub(NormandyApi, "fetchRecipes"),
withStub(ActionsManager.prototype, "fetchRemoteActions"),
withStub(ActionsManager.prototype, "preExecution"),
withStub(ActionsManager.prototype, "runRecipe"),
withStub(ActionsManager.prototype, "finalize"),
withStub(Uptake, "reportRecipe"),
async function testRun(
reportRunnerStub,
fetchRecipesStub,
fetchRemoteActionsStub,
preExecutionStub,
runRecipeStub,
finalizeStub,
reportRecipeStub,
@ -176,8 +151,6 @@ decorate_task(
await RecipeRunner.run();
ok(fetchRemoteActionsStub.calledOnce, "remote actions should be fetched");
ok(preExecutionStub.calledOnce, "pre-execution hooks should be run");
Assert.deepEqual(
runRecipeStub.args,
[[matchRecipe], [missingRecipe]],
@ -207,13 +180,11 @@ decorate_task(
}),
withStub(NormandyApi, "verifyObjectSignature"),
withStub(ActionsManager.prototype, "runRecipe"),
withStub(ActionsManager.prototype, "fetchRemoteActions"),
withStub(ActionsManager.prototype, "finalize"),
withStub(Uptake, "reportRecipe"),
async function testReadFromRemoteSettings(
verifyObjectSignatureStub,
runRecipeStub,
fetchRemoteActionsStub,
finalizeStub,
reportRecipeStub,
) {
@ -290,31 +261,24 @@ decorate_task(
withMockNormandyApi,
async function testRunFetchFail(mockApi) {
const reportRunner = sinon.stub(Uptake, "reportRunner");
const action = {name: "action"};
mockApi.actions = [action];
mockApi.fetchRecipes.rejects(new Error("Signature not valid"));
await withMockActionSandboxManagers(mockApi.actions, async managers => {
const manager = managers.action;
await RecipeRunner.run();
await RecipeRunner.run();
// If the recipe fetch failed, do not run anything.
sinon.assert.notCalled(manager.runAsyncCallback);
sinon.assert.calledWith(reportRunner, Uptake.RUNNER_SERVER_ERROR);
// If the recipe fetch failed, report a server error
sinon.assert.calledWith(reportRunner, Uptake.RUNNER_SERVER_ERROR);
// Test that network errors report a specific uptake error
reportRunner.reset();
mockApi.fetchRecipes.rejects(new Error("NetworkError: The system was down"));
await RecipeRunner.run();
sinon.assert.calledWith(reportRunner, Uptake.RUNNER_NETWORK_ERROR);
// Test that network errors report a specific uptake error
reportRunner.reset();
mockApi.fetchRecipes.rejects(new Error("NetworkError: The system was down"));
await RecipeRunner.run();
sinon.assert.calledWith(reportRunner, Uptake.RUNNER_NETWORK_ERROR);
// Test that signature issues report a specific uptake error
reportRunner.reset();
mockApi.fetchRecipes.rejects(new NormandyApi.InvalidSignatureError("Signature fail"));
await RecipeRunner.run();
sinon.assert.calledWith(reportRunner, Uptake.RUNNER_INVALID_SIGNATURE);
});
// Test that signature issues report a specific uptake error
reportRunner.reset();
mockApi.fetchRecipes.rejects(new NormandyApi.InvalidSignatureError("Signature fail"));
await RecipeRunner.run();
sinon.assert.calledWith(reportRunner, Uptake.RUNNER_INVALID_SIGNATURE);
reportRunner.restore();
}

Просмотреть файл

@ -1,7 +1,6 @@
"use strict";
ChromeUtils.import("resource://normandy/lib/Storage.jsm", this);
ChromeUtils.import("resource://normandy/lib/SandboxManager.jsm", this);
add_task(async function() {
const store1 = new Storage("prefix1");

Просмотреть файл

@ -2,8 +2,6 @@ ChromeUtils.import("resource://gre/modules/Preferences.jsm", this);
ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", this);
ChromeUtils.import("resource://testing-common/TestUtils.jsm", this);
ChromeUtils.import("resource://normandy-content/AboutPages.jsm", this);
ChromeUtils.import("resource://normandy/lib/SandboxManager.jsm", this);
ChromeUtils.import("resource://normandy/lib/NormandyDriver.jsm", this);
ChromeUtils.import("resource://normandy/lib/NormandyApi.jsm", this);
ChromeUtils.import("resource://normandy/lib/TelemetryEvents.jsm", this);
ChromeUtils.defineModuleGetter(this, "TelemetryTestUtils",
@ -100,30 +98,6 @@ this.withInstalledWebExtension = function(manifestOverrides = {}, expectUninstal
};
};
this.withSandboxManager = function(Assert) {
return function wrapper(testFunction) {
return async function wrappedTestFunction(...args) {
const sandboxManager = new SandboxManager();
sandboxManager.addHold("test running");
await testFunction(...args, sandboxManager);
sandboxManager.removeHold("test running");
await sandboxManager.isNuked()
.then(() => Assert.ok(true, "sandbox is nuked"))
.catch(e => Assert.ok(false, "sandbox is nuked", e));
};
};
};
this.withDriver = function(Assert, testFunction) {
return withSandboxManager(Assert)(async function inner(...args) {
const sandboxManager = args[args.length - 1];
const driver = new NormandyDriver(sandboxManager);
await testFunction(driver, ...args);
});
};
this.withMockNormandyApi = function(testFunction) {
return async function inner(...args) {
const mockApi = {actions: [], recipes: [], implementations: {}, extensionDetails: {}};

Просмотреть файл

@ -1,110 +0,0 @@
"use strict";
const {SandboxManager} = ChromeUtils.import("resource://normandy/lib/SandboxManager.jsm");
// wrapAsync should wrap privileged Promises with Promises that are usable by
// the sandbox.
add_task(async function() {
const manager = new SandboxManager();
manager.addHold("testing");
manager.cloneIntoGlobal("driver", {
async privileged() {
return "privileged";
},
wrapped: manager.wrapAsync(async function() {
return "wrapped";
}),
aValue: "aValue",
wrappedThis: manager.wrapAsync(async function() {
return this.aValue;
}),
}, {cloneFunctions: true});
// Assertion helpers
manager.addGlobal("ok", ok);
manager.addGlobal("equal", equal);
const sandboxResult = await new Promise(resolve => {
manager.addGlobal("resolve", result => resolve(result));
manager.evalInSandbox(`
// Unwrapped privileged promises are not accessible in the sandbox
try {
const privilegedResult = driver.privileged().then(() => false);
ok(false, "The sandbox could not use a privileged Promise");
} catch (err) { }
// Wrapped functions return promises that the sandbox can access.
const wrappedResult = driver.wrapped();
ok("then" in wrappedResult);
// Resolve the Promise around the sandbox with the wrapped result to test
// that the Promise in the sandbox works.
wrappedResult.then(resolve);
`);
});
equal(sandboxResult, "wrapped", "wrapAsync methods return Promises that work in the sandbox");
await manager.evalInSandbox(`
(async function sandboxTest() {
equal(
await driver.wrappedThis(),
"aValue",
"wrapAsync preserves the behavior of the this keyword",
);
})();
`);
manager.removeHold("testing");
});
// wrapAsync cloning options
add_task(async function() {
const manager = new SandboxManager();
manager.addHold("testing");
// clonedArgument stores the argument passed to cloned(), which we use to test
// that arguments from within the sandbox are cloned outside.
let clonedArgument = null;
manager.cloneIntoGlobal("driver", {
uncloned: manager.wrapAsync(async function() {
return {value: "uncloned"};
}),
cloned: manager.wrapAsync(async function(argument) {
clonedArgument = argument;
return {value: "cloned"};
}, {cloneInto: true, cloneArguments: true}),
}, {cloneFunctions: true});
// Assertion helpers
manager.addGlobal("ok", ok);
manager.addGlobal("deepEqual", deepEqual);
await new Promise(resolve => {
manager.addGlobal("resolve", resolve);
manager.evalInSandbox(`
(async function() {
// The uncloned return value should be privileged and inaccesible.
const uncloned = await driver.uncloned();
ok(!("value" in uncloned), "The sandbox could not use an uncloned return value");
// The cloned return value should be usable.
deepEqual(
await driver.cloned({value: "insidesandbox"}),
{value: "cloned"},
"The sandbox could use the cloned return value",
);
})().then(resolve);
`);
});
// Removing the hold nukes the sandbox. Afterwards, because cloned() has the
// cloneArguments option, the clonedArgument variable should still be
// accessible.
manager.removeHold("testing");
deepEqual(
clonedArgument,
{value: "insidesandbox"},
"cloneArguments allowed an argument from within the sandbox to persist after it was nuked",
);
});

Просмотреть файл

@ -11,4 +11,3 @@ tags = normandy
[test_addon_unenroll.js]
[test_NormandyApi.js]
[test_SandboxManager.js]