Bug 1606883 - Remove legacy method of fetching Normandy recipes directly from the server r=leplatrem

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Michael Cooper 2020-01-13 17:24:50 +00:00
Родитель 7b485ae170
Коммит 1469d0cf85
8 изменённых файлов: 133 добавлений и 433 удалений

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

@ -208,6 +208,12 @@ var whitelist = [
isFromDevTools: true,
},
{ file: "chrome://devtools/skin/images/next.svg", isFromDevTools: true },
// Feature gates are available but not used yet - Bug 1479127
{ file: "resource://featuregates/FeatureGate.jsm" },
{
file: "resource://featuregates/FeatureGateImplementation.jsm",
},
{ file: "resource://featuregates/feature_definitions.json" },
// Bug 1526672
{
file: "resource://app/localization/en-US/browser/touchbar/touchbar.ftl",

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

@ -1,10 +1,10 @@
[normandy-remote-settings]
title = "Normandy: Remote Settings transport"
description = """
When enabled, the Normandy client will fetch recipes from Remote Settings \
instead of directly from the Normandy API."""
# there are currently no feature gates
[demo-feature]
title = "Demo Feature"
description = "A no-op feature to demo the feature gate system."
restart-required = false
preference = "foo.bar.baz"
type = "boolean"
bug-numbers = [1519276]
bug-numbers = [1479127]
is-public = true
default-value = true
default-value = false

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

@ -13,13 +13,11 @@ and then executes them.
.. note::
Originally, the recipes were fetched from the `recipe server`_, but in `Bug 1513854`_
Previously, the recipes were fetched from the `recipe server`_, but in `Bug 1513854`_
the source was changed to *Remote Settings*. The cryptographic signatures are verified
at the *Remote Settings* level (integrity) and at the *Normandy* level
(authenticity of publisher).
The source can still be controlled by :ref:`Feature Gates <components/featuregates>`.
.. _recipe server: https://github.com/mozilla/normandy/
.. _Bug 1513854: https://bugzilla.mozilla.org/show_bug.cgi?id=1513854

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

@ -8,9 +8,6 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { LogManager } = ChromeUtils.import(
"resource://normandy/lib/LogManager.jsm"
);
ChromeUtils.defineModuleGetter(
this,
@ -25,7 +22,6 @@ XPCOMUtils.defineLazyGlobalGetters(this, [
var EXPORTED_SYMBOLS = ["NormandyApi"];
const log = LogManager.getLogger("normandy-api");
const prefs = Services.prefs.getBranch("app.normandy.");
let indexPromise = null;
@ -37,31 +33,18 @@ var NormandyApi = {
indexPromise = null;
},
apiCall(method, endpoint, data = {}) {
get(endpoint, data) {
const url = new URL(endpoint);
method = method.toLowerCase();
let body = undefined;
if (data) {
if (method === "get") {
for (const key of Object.keys(data)) {
url.searchParams.set(key, data[key]);
}
} else if (method === "post") {
body = JSON.stringify(data);
for (const key of Object.keys(data)) {
url.searchParams.set(key, data[key]);
}
}
const headers = { Accept: "application/json" };
return fetch(url.href, { method, body, headers, credentials: "omit" });
},
get(endpoint, data) {
return this.apiCall("get", endpoint, data);
},
post(endpoint, data) {
return this.apiCall("post", endpoint, data);
return fetch(url.href, {
method: "get",
headers: { Accept: "application/json" },
credentials: "omit",
});
},
absolutify(url) {
@ -92,33 +75,6 @@ var NormandyApi = {
return this.absolutify(url);
},
async fetchSignedObjects(type, filters) {
const signedObjectsUrl = await this.getApiUrl(`${type}-signed`);
const objectsResponse = await this.get(signedObjectsUrl, filters);
const rawText = await objectsResponse.text();
const objectsWithSigs = JSON.parse(rawText);
return Promise.all(
objectsWithSigs.map(async item => {
// Check that the rawtext (the object and the signature)
// includes the CanonicalJSON version of the object. This isn't
// strictly needed, but it is a great benefit for debugging
// signature problems.
const object = item[type];
const serialized = CanonicalJSON.stringify(object);
if (!rawText.includes(serialized)) {
log.debug(rawText, serialized);
throw new NormandyApi.InvalidSignatureError(
`Canonical ${type} serialization does not match!`
);
}
// Verify content signature using cryptography (will throw if fails).
await this.verifyObjectSignature(serialized, item.signature, type);
return object;
})
);
},
/**
* Verify content signature, by serializing the specified `object` as
* canonical JSON, and using the Normandy signer verifier to check that
@ -178,15 +134,6 @@ var NormandyApi = {
return clientData;
},
/**
* Fetch an array of available actions from the server.
* @resolves {Array}
*/
async fetchRecipes() {
const filters = { enabled: true, only_baseline_capabilities: false };
return this.fetchSignedObjects("recipe", filters);
},
/**
* Fetch details for an extension from the server.
* @param extensionId {integer} The ID of the extension to look up

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

@ -21,7 +21,6 @@ XPCOMUtils.defineLazyServiceGetter(
XPCOMUtils.defineLazyModuleGetters(this, {
RemoteSettings: "resource://services-settings/remote-settings.js",
FeatureGate: "resource://featuregates/FeatureGate.jsm",
Storage: "resource://normandy/lib/Storage.jsm",
FilterExpressions:
"resource://gre/modules/components-utils/FilterExpressions.jsm",
@ -65,10 +64,6 @@ XPCOMUtils.defineLazyGetter(this, "gRemoteSettingsClient", () => {
});
});
XPCOMUtils.defineLazyGetter(this, "gRemoteSettingsGate", () => {
return FeatureGate.fromId("normandy-remote-settings");
});
/**
* cacheProxy returns an object Proxy that will memoize properties of the target.
*/
@ -100,7 +95,7 @@ var RecipeRunner = {
this.checkPrefs(); // sets this.enabled
this.watchPrefs();
await this.setUpRemoteSettings();
this.setUpRemoteSettings();
// Here "first run" means the first run this profile has ever done. This
// preference is set to true at the end of this function, and never reset to
@ -131,10 +126,7 @@ var RecipeRunner = {
// This is not needed for the first run case, because remote settings
// already handles empty collections well.
if (devMode) {
let remoteSettingsGate = await gRemoteSettingsGate;
if (await remoteSettingsGate.isEnabled()) {
await gRemoteSettingsClient.sync();
}
await gRemoteSettingsClient.sync();
}
let trigger;
if (devMode) {
@ -252,70 +244,52 @@ var RecipeRunner = {
timerManager.unregisterTimer(TIMER_NAME);
},
async setUpRemoteSettings() {
const remoteSettingsGate = await gRemoteSettingsGate;
if (await remoteSettingsGate.isEnabled()) {
this.attachRemoteSettings();
setUpRemoteSettings() {
if (this._alreadySetUpRemoteSettings) {
return;
}
const observer = {
onEnable: this.attachRemoteSettings.bind(this),
onDisable: this.detachRemoteSettings.bind(this),
};
remoteSettingsGate.addObserver(observer);
CleanupManager.addCleanupHandler(() =>
remoteSettingsGate.removeObserver(observer)
);
},
this._alreadySetUpRemoteSettings = true;
attachRemoteSettings() {
this.loadFromRemoteSettings = true;
if (!this._onSync) {
this._onSync = async () => {
if (!this.enabled) {
return;
}
// Delay the Normandy run by a random amount, determined by preference.
// This helps alleviate server load, since we don't have a thundering
// herd of users trying to update all at once.
if (this._syncSkewTimeout) {
clearTimeout(this._syncSkewTimeout);
}
let minSkewSec = 1; // this is primarily is to avoid race conditions in tests
let maxSkewSec = Services.prefs.getIntPref(ONSYNC_SKEW_SEC_PREF, 0);
if (maxSkewSec >= minSkewSec) {
let skewMillis =
(minSkewSec + Math.random() * (maxSkewSec - minSkewSec)) * 1000;
log.debug(
`Delaying on-sync Normandy run for ${Math.floor(
skewMillis / 1000
)} seconds`
);
this._syncSkewTimeout = setTimeout(
() => this.run({ trigger: "sync" }),
skewMillis
);
} else {
log.debug(`Not skewing on-sync Normandy run`);
await this.run({ trigger: "sync" });
}
};
gRemoteSettingsClient.on("sync", this._onSync);
this._onSync = this.onSync.bind(this);
}
gRemoteSettingsClient.on("sync", this._onSync);
CleanupManager.addCleanupHandler(() => {
gRemoteSettingsClient.off("sync", this._onSync);
this._alreadySetUpRemoteSettings = false;
});
},
detachRemoteSettings() {
this.loadFromRemoteSettings = false;
if (this._onSync) {
// Ignore if no event listener was setup or was already removed (ie. pref changed while enabled).
gRemoteSettingsClient.off("sync", this._onSync);
this._onSync = null;
/** Called when our Remote Settings collection is updated */
async onSync() {
if (!this.enabled) {
return;
}
// Delay the Normandy run by a random amount, determined by preference.
// This helps alleviate server load, since we don't have a thundering
// herd of users trying to update all at once.
if (this._syncSkewTimeout) {
clearTimeout(this._syncSkewTimeout);
this._syncSkewTimeout = null;
}
let minSkewSec = 1; // this is primarily is to avoid race conditions in tests
let maxSkewSec = Services.prefs.getIntPref(ONSYNC_SKEW_SEC_PREF, 0);
if (maxSkewSec >= minSkewSec) {
let skewMillis =
(minSkewSec + Math.random() * (maxSkewSec - minSkewSec)) * 1000;
log.debug(
`Delaying on-sync Normandy run for ${Math.floor(
skewMillis / 1000
)} seconds`
);
this._syncSkewTimeout = setTimeout(
() => this.run({ trigger: "sync" }),
skewMillis
);
} else {
log.debug(`Not skewing on-sync Normandy run`);
await this.run({ trigger: "sync" });
}
},
@ -357,8 +331,6 @@ var RecipeRunner = {
try {
recipesToRun = await this.loadRecipes();
} catch (e) {
// Either we failed at fetching the recipes from server (legacy),
// or the recipes signature verification failed.
let status = Uptake.RUNNER_SERVER_ERROR;
if (/NetworkError/.test(e)) {
status = Uptake.RUNNER_NETWORK_ERROR;
@ -399,44 +371,17 @@ var RecipeRunner = {
* Return the list of recipes to run, filtered for the current environment.
*/
async loadRecipes() {
// If RemoteSettings is enabled, we read the list of recipes from there.
// The recipe filtering is done via the provided callback (see `gRemoteSettingsClient`).
if (this.loadFromRemoteSettings) {
// First, fetch recipes that should run on this client.
const entries = await gRemoteSettingsClient.get();
// Then, verify the signature of each recipe. It will throw if invalid.
return Promise.all(
entries.map(async ({ recipe, signature }) => {
await NormandyApi.verifyObjectSignature(recipe, signature, "recipe");
return recipe;
})
);
}
// Obtain the recipes from the Normandy server (legacy).
let recipes;
try {
recipes = await NormandyApi.fetchRecipes();
log.debug(
`Fetched ${recipes.length} recipes from the server: ` +
recipes.map(r => r.name).join(", ")
);
} catch (e) {
const apiUrl = Services.prefs.getCharPref(API_URL_PREF);
log.error(`Could not fetch recipes from ${apiUrl}: "${e}"`);
throw e;
}
// Check if each recipe should be run, according to `shouldRunRecipe`. This
// can't be a simple call to `Array.filter` because checking if a recipe
// should run is an async operation.
const recipesToRun = [];
for (const recipe of recipes) {
if (await this.shouldRunRecipe(recipe)) {
recipesToRun.push(recipe);
}
}
return recipesToRun;
// Fetch recipes that should run on this client. Then, verify the signature
// of each recipe. The recipe filtering is done implicitly by the callback
// provided to `gRemoteSettingsClient`.
const entries = await gRemoteSettingsClient.get();
return Promise.all(
entries.map(async ({ recipe, signature }) => {
// this will throw if the signature is invalid
await NormandyApi.verifyObjectSignature(recipe, signature, "recipe");
return recipe;
})
);
},
getFilterContext(recipe) {

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

@ -223,76 +223,6 @@ decorate_task(
);
decorate_task(
withPrefEnv({
set: [["features.normandy-remote-settings.enabled", false]],
}),
withStub(Uptake, "reportRunner"),
withStub(NormandyApi, "fetchRecipes"),
withStub(ActionsManager.prototype, "runRecipe"),
withStub(ActionsManager.prototype, "finalize"),
withStub(Uptake, "reportRecipe"),
async function testRun(
reportRunnerStub,
fetchRecipesStub,
runRecipeStub,
finalizeStub,
reportRecipeStub
) {
const runRecipeReturn = Promise.resolve();
const runRecipeReturnThen = sinon.spy(runRecipeReturn, "then");
runRecipeStub.returns(runRecipeReturn);
const matchRecipe = {
id: "match",
action: "matchAction",
filter_expression: "true",
};
const noMatchRecipe = {
id: "noMatch",
action: "noMatchAction",
filter_expression: "false",
};
const missingRecipe = {
id: "missing",
action: "missingAction",
filter_expression: "true",
};
fetchRecipesStub.callsFake(async () => [
matchRecipe,
noMatchRecipe,
missingRecipe,
]);
await RecipeRunner.run();
Assert.deepEqual(
runRecipeStub.args,
[[matchRecipe], [missingRecipe]],
"recipe with matching filters should be executed"
);
ok(
runRecipeReturnThen.called,
"the run method should be used asyncronously"
);
// Test uptake reporting
Assert.deepEqual(
reportRunnerStub.args,
[[Uptake.RUNNER_SUCCESS]],
"RecipeRunner should report uptake telemetry"
);
Assert.deepEqual(
reportRecipeStub.args,
[[noMatchRecipe, Uptake.RECIPE_DIDNT_MATCH_FILTER]],
"Filtered-out recipes should be reported"
);
}
);
decorate_task(
withPrefEnv({
set: [["features.normandy-remote-settings.enabled", true]],
}),
withStub(RecipeRunner, "getCapabilities"),
async function test_run_includesCapabilities(getCapabilitiesStub) {
const rsCollection = await RecipeRunner._remoteSettingsClientForTesting.openCollection();
@ -313,17 +243,12 @@ decorate_task(
);
decorate_task(
withPrefEnv({
set: [["features.normandy-remote-settings.enabled", true]],
}),
withStub(NormandyApi, "verifyObjectSignature"),
withSpy(NormandyApi, "fetchRecipes"),
withStub(ActionsManager.prototype, "runRecipe"),
withStub(ActionsManager.prototype, "finalize"),
withStub(Uptake, "reportRecipe"),
async function testReadFromRemoteSettings(
verifyObjectSignatureStub,
fetchRecipesSpy,
runRecipeStub,
finalizeStub,
reportRecipeStub
@ -379,15 +304,10 @@ decorate_task(
[[noMatchRecipe, Uptake.RECIPE_DIDNT_MATCH_FILTER]],
"Filtered-out recipes should be reported"
);
ok(fetchRecipesSpy.notCalled, "fetchRecipes should not be called");
}
);
decorate_task(
withPrefEnv({
set: [["features.normandy-remote-settings.enabled", true]],
}),
withStub(NormandyApi, "verifyObjectSignature"),
withStub(ActionsManager.prototype, "runRecipe"),
withStub(RecipeRunner, "getCapabilities"),
@ -433,9 +353,6 @@ decorate_task(
);
decorate_task(
withPrefEnv({
set: [["features.normandy-remote-settings.enabled", true]],
}),
withStub(ActionsManager.prototype, "runRecipe"),
withStub(NormandyApi, "get"),
withStub(Uptake, "reportRunner"),
@ -477,40 +394,6 @@ decorate_task(
}
);
decorate_task(
withPrefEnv({
set: [["features.normandy-remote-settings.enabled", false]],
}),
withMockNormandyApi,
async function testRunFetchFail(mockApi) {
const reportRunner = sinon.stub(Uptake, "reportRunner");
mockApi.fetchRecipes.rejects(new Error("Signature not valid"));
await RecipeRunner.run();
// 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 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();
}
);
// Test init() during normal operation
decorate_task(
withPrefEnv({
@ -715,72 +598,45 @@ decorate_task(
decorate_task(
withPrefEnv({
set: [
["features.normandy-remote-settings.enabled", false],
["app.normandy.onsync_skew_sec", 0],
],
set: [["app.normandy.onsync_skew_sec", 0]],
}),
withStub(RecipeRunner, "run"),
async function testRunOnSyncRemoteSettings(runStub) {
const rsClient = RecipeRunner._remoteSettingsClientForTesting;
await RecipeRunner.init();
ok(
RecipeRunner._alreadySetUpRemoteSettings,
"remote settings should be set up in the runner"
);
// Runner disabled + pref off.
// Runner disabled
RecipeRunner.disable();
await rsClient.emit("sync", {});
ok(!runStub.called, "run() should not be called if disabled");
runStub.reset();
// Runner enabled + pref off.
// Runner enabled
RecipeRunner.enable();
await rsClient.emit("sync", {});
ok(!runStub.called, "run() should not be called if pref not set");
ok(runStub.called, "run() should be called if enabled");
runStub.reset();
await SpecialPowers.pushPrefEnv({
set: [["features.normandy-remote-settings.enabled", true]],
});
// Runner enabled + pref on.
await rsClient.emit("sync", {});
ok(runStub.called, "run() should be called if pref is set");
runStub.reset();
// Runner disabled + pref on.
// Runner disabled
RecipeRunner.disable();
await rsClient.emit("sync", {});
ok(!runStub.called, "run() should not be called if disabled with pref set");
ok(!runStub.called, "run() should not be called if disabled");
runStub.reset();
// Runner re-enabled + pref on.
// Runner re-enabled
RecipeRunner.enable();
await rsClient.emit("sync", {});
ok(
runStub.called,
"run() should be called at most once if runner is re-enabled"
);
runStub.reset();
await SpecialPowers.pushPrefEnv({
set: [["features.normandy-remote-settings.enabled", false]],
});
// Runner enabled + pref off.
await rsClient.emit("sync", {});
ok(!runStub.called, "run() should not be called if pref is unset");
runStub.reset();
// Runner disabled + pref off.
RecipeRunner.disable();
await rsClient.emit("sync", {});
ok(!runStub.called, "run() should still not be called if disabled");
RecipeRunner.enable();
ok(runStub.called, "run() should be called if runner is re-enabled");
}
);
decorate_task(
withPrefEnv({
set: [
["features.normandy-remote-settings.enabled", true],
["app.normandy.onsync_skew_sec", 600], // 10 minutes, much longer than the test will take to run
],
}),
@ -816,7 +672,6 @@ decorate_task(
decorate_task(
withPrefEnv({
set: [
["features.normandy-remote-settings.enabled", true],
// Enable update timer logs.
["app.update.log", true],
["app.normandy.onsync_skew_sec", 0],
@ -849,6 +704,8 @@ decorate_task(
RecipeRunner.unregisterTimer();
RecipeRunner.registerTimer();
is(loadRecipesStub.callCount, 0, "run() shouldn't have run yet");
// Simulate timer notification.
const service = Cc["@mozilla.org/updates/timer-manager;1"].getService(
Ci.nsITimerCallback
@ -865,21 +722,21 @@ decorate_task(
await endPromise; // will timeout if run() not called.
const timerLatency = Date.now() - startTime;
is(loadRecipesStub.callCount, 1, "run() should be called from timer");
// Run once from sync event.
const rsClient = RecipeRunner._remoteSettingsClientForTesting;
await rsClient.emit("sync", {}); // waits for listeners to run.
is(loadRecipesStub.callCount, 2, "run() should be called from sync");
// Run timer again.
service.notify(newTimer());
// Wait at least as long as the latency we had above. Ten times as a margin.
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, timerLatency * 10));
is(
loadRecipesStub.callCount,
2,
"run() does not run again from timer after sync"
);
is(loadRecipesStub.callCount, 2, "run() does not run again from timer");
}
);

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

@ -131,9 +131,6 @@ this.withMockNormandyApi = function(testFunction) {
};
// Use callsFake instead of resolves so that the current values in mockApi are used.
mockApi.fetchRecipes = sinon
.stub(NormandyApi, "fetchRecipes")
.callsFake(async () => mockApi.recipes);
mockApi.fetchExtensionDetails = sinon
.stub(NormandyApi, "fetchExtensionDetails")
.callsFake(async extensionId => {
@ -147,7 +144,6 @@ this.withMockNormandyApi = function(testFunction) {
try {
await testFunction(...args, mockApi);
} finally {
mockApi.fetchRecipes.restore();
mockApi.fetchExtensionDetails.restore();
}
};

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

@ -81,85 +81,42 @@ add_task(
})
);
add_task(
withMockApiServer(async function test_fetchRecipes() {
const recipes = await NormandyApi.fetchRecipes();
equal(recipes.length, 1);
equal(recipes[0].name, "system-addon-test");
})
);
add_task(async function test_fetchSignedObjects_canonical_mismatch() {
const getApiUrl = sinon.stub(NormandyApi, "getApiUrl");
// The object is non-canonical (it has whitespace, properties are out of order)
const response = new MockResponse(`[
{
"object": {"b": 1, "a": 2},
"signature": {"signature": "", "x5u": ""}
}
]`);
const get = sinon.stub(NormandyApi, "get").resolves(response);
try {
await NormandyApi.fetchSignedObjects("object");
ok(false, "fetchSignedObjects did not throw for canonical JSON mismatch");
} catch (err) {
ok(
err instanceof NormandyApi.InvalidSignatureError,
"Error is an InvalidSignatureError"
);
ok(/Canonical/.test(err), "Error is due to canonical JSON mismatch");
}
getApiUrl.restore();
get.restore();
});
// Test validation errors due to validation throwing an exception (e.g. when
// parameters passed to validation are malformed).
add_task(
withMockApiServer(async function test_fetchSignedObjects_validation_error() {
const getApiUrl = sinon
.stub(NormandyApi, "getApiUrl")
.resolves("http://localhost/object/");
// Mock two URLs: object and the x5u
const get = sinon.stub(NormandyApi, "get").callsFake(async url => {
if (url.endsWith("object/")) {
return new MockResponse(
CanonicalJSON.stringify([
{
object: { a: 1, b: 2 },
signature: {
signature: "invalidsignature",
x5u: "http://localhost/x5u/",
},
},
])
);
} else if (url.endsWith("x5u/")) {
withMockApiServer(
async function test_validateSignedObject_validation_error() {
// Mock the x5u URL
const getStub = sinon.stub(NormandyApi, "get").callsFake(async url => {
ok(url.endsWith("x5u/"), "the only request should be to fetch the x5u");
return new MockResponse("certchain");
});
const signedObject = { a: 1, b: 2 };
const signature = {
signature: "invalidsignature",
x5u: "http://localhost/x5u/",
};
// Validation should fail due to a malformed x5u and signature.
try {
await NormandyApi.verifyObjectSignature(
signedObject,
signature,
"object"
);
ok(false, "validateSignedObject did not throw for a validation error");
} catch (err) {
ok(
err instanceof NormandyApi.InvalidSignatureError,
"Error is an InvalidSignatureError"
);
ok(/signature/.test(err), "Error is due to a validation error");
}
return null;
});
// Validation should fail due to a malformed x5u and signature.
try {
await NormandyApi.fetchSignedObjects("object");
ok(false, "fetchSignedObjects did not throw for a validation error");
} catch (err) {
ok(
err instanceof NormandyApi.InvalidSignatureError,
"Error is an InvalidSignatureError"
);
ok(/signature/.test(err), "Error is due to a validation error");
getStub.restore();
}
getApiUrl.restore();
get.restore();
})
)
);
// Test validation errors due to validation returning false (e.g. when parameters
@ -170,10 +127,20 @@ const invalidSignatureServer = makeMockApiServer(
add_task(
withServer(
invalidSignatureServer,
async function test_fetchSignedObjects_invalid_signature() {
async function test_verifySignedObject_invalid_signature() {
// Get the test recipe and signature from the mock server.
const recipesUrl = await NormandyApi.getApiUrl("recipe-signed");
const recipeResponse = await NormandyApi.get(recipesUrl);
const recipes = await recipeResponse.json();
equal(recipes.length, 1, "Test data has one recipe");
const [{ recipe, signature }] = recipes;
try {
await NormandyApi.fetchSignedObjects("recipe");
ok(false, "fetchSignedObjects did not throw for an invalid signature");
await NormandyApi.verifyObjectSignature(recipe, signature, "recipe");
ok(
false,
"verifyObjectSignature did not throw for an invalid signature"
);
} catch (err) {
ok(
err instanceof NormandyApi.InvalidSignatureError,
@ -244,22 +211,6 @@ add_task(
})
);
add_task(
withScriptServer("query_server.sjs", async function test_postData(serverUrl) {
// Test that NormandyApi can POST JSON-formatted data to the test server.
const response = await NormandyApi.post(serverUrl, {
foo: "bar",
baz: "biff",
});
const data = await response.json();
Assert.deepEqual(
data,
{ queryString: {}, body: { foo: "bar", baz: "biff" } },
"NormandyApi sent an incorrect query string."
);
})
);
// Test that no credentials are sent, even if the cookie store contains them.
add_task(
withScriptServer("cookie_server.sjs", async function test_sendsNoCredentials(