зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
98d200e4b4
Коммит
490749ff91
|
@ -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]
|
||||
|
|
Загрузка…
Ссылка в новой задаче