gecko-dev/toolkit/components/normandy/test/browser/browser_LegacyHeartbeat.js
Barret Rennie 9ef1c3a9cb Bug 1829412 - Simplify NimbusTestUtils.enrollmentHelper r=chumphreys,settings-reviewers,pip-reviewers,credential-management-reviewers,search-reviewers,anti-tracking-reviewers,omc-reviewers,home-newtab-reviewers,thecount,issammani,aminomancer,mconley
The enrollmentHelper was much more complicated than it needed to be. The
internal asynchrony that required awaiting an additional promise was fixed in
bug 1773583.

The returned cleanup function is no longer async, so unnecessary awaits have
been removed. This also applies to enrollWithFeatureConfig, as it is a wrapper
around enrollmentHelper.

Differential Revision: https://phabricator.services.mozilla.com/D212318
2024-06-06 14:42:00 +00:00

224 lines
5.7 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { BaseAction } = ChromeUtils.importESModule(
"resource://normandy/actions/BaseAction.sys.mjs"
);
const { ClientEnvironment } = ChromeUtils.importESModule(
"resource://normandy/lib/ClientEnvironment.sys.mjs"
);
const { EventEmitter } = ChromeUtils.importESModule(
"resource://normandy/lib/EventEmitter.sys.mjs"
);
const { Heartbeat } = ChromeUtils.importESModule(
"resource://normandy/lib/Heartbeat.sys.mjs"
);
const { Normandy } = ChromeUtils.importESModule(
"resource://normandy/Normandy.sys.mjs"
);
const { ExperimentAPI } = ChromeUtils.importESModule(
"resource://nimbus/ExperimentAPI.sys.mjs"
);
const { ExperimentFakes } = ChromeUtils.importESModule(
"resource://testing-common/NimbusTestUtils.sys.mjs"
);
const { RecipeRunner } = ChromeUtils.importESModule(
"resource://normandy/lib/RecipeRunner.sys.mjs"
);
const { RemoteSettings } = ChromeUtils.importESModule(
"resource://services-settings/remote-settings.sys.mjs"
);
const { JsonSchema } = ChromeUtils.importESModule(
"resource://gre/modules/JsonSchema.sys.mjs"
);
const SURVEY = {
surveyId: "a survey",
message: "test message",
engagementButtonLabel: "",
thanksMessage: "thanks!",
postAnswerUrl: "https://example.com",
learnMoreMessage: "Learn More",
learnMoreUrl: "https://example.com",
repeatOption: "once",
};
// See properties.payload in
// https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/main/schemas/telemetry/heartbeat/heartbeat.4.schema.json
const PAYLOAD_SCHEMA = {
additionalProperties: false,
anyOf: [
{
required: ["closedTS"],
},
{
required: ["windowClosedTS"],
},
],
properties: {
closedTS: {
minimum: 0,
type: "integer",
},
engagedTS: {
minimum: 0,
type: "integer",
},
expiredTS: {
minimum: 0,
type: "integer",
},
flowId: {
pattern:
"^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
type: "string",
},
learnMoreTS: {
minimum: 0,
type: "integer",
},
offeredTS: {
minimum: 0,
type: "integer",
},
score: {
minimum: 1,
type: "integer",
},
surveyId: {
type: "string",
},
surveyVersion: {
pattern: "^([0-9]+|[a-fA-F0-9]{64})$",
type: "string",
},
testing: {
type: "boolean",
},
version: {
maximum: 1,
minimum: 1,
type: "number",
},
votedTS: {
minimum: 0,
type: "integer",
},
windowClosedTS: {
minimum: 0,
type: "integer",
},
},
required: ["version", "flowId", "offeredTS", "surveyId", "surveyVersion"],
type: "object",
};
function assertSurvey(actual, expected) {
for (const key of Object.keys(actual)) {
if (["flowId", "postAnswerUrl", "surveyVersion"].includes(key)) {
continue;
}
Assert.equal(
actual[key],
expected[key],
`Heartbeat should receive correct ${key} parameter`
);
}
Assert.equal(actual.surveyVersion, "1");
Assert.ok(actual.postAnswerUrl.startsWith(expected.postAnswerUrl));
}
decorate_task(
withStubbedHeartbeat(),
withClearStorage(),
async function testLegacyHeartbeatTrigger({ heartbeatClassStub }) {
const sandbox = sinon.createSandbox();
const doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
featureId: "legacyHeartbeat",
value: {
survey: SURVEY,
},
});
const client = RemoteSettings("normandy-recipes-capabilities");
sandbox.stub(client, "get").resolves([]);
try {
await RecipeRunner.run();
Assert.equal(
heartbeatClassStub.args.length,
1,
"Heartbeat should be instantiated once"
);
assertSurvey(heartbeatClassStub.args[0][1], SURVEY);
doEnrollmentCleanup();
} finally {
sandbox.restore();
}
}
);
decorate_task(
withClearStorage(),
async function testLegacyHeartbeatPingPayload() {
const sandbox = sinon.createSandbox();
const doEnrollmentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
featureId: "legacyHeartbeat",
value: {
survey: SURVEY,
},
});
const client = RemoteSettings("normandy-recipes-capabilities");
sandbox.stub(client, "get").resolves([]);
// Override Heartbeat so we can get the instance and manipulate it directly.
const heartbeatDeferred = Promise.withResolvers();
class TestHeartbeat extends Heartbeat {
constructor(...args) {
super(...args);
heartbeatDeferred.resolve(this);
}
}
ShowHeartbeatAction.overrideHeartbeatForTests(TestHeartbeat);
try {
await RecipeRunner.run();
const heartbeat = await heartbeatDeferred.promise;
// We are going to simulate the timer timing out, so we do not want it to
// *actually* time out.
heartbeat.endTimerIfPresent("surveyEndTimer");
const notice = await heartbeat.noticePromise;
await notice.updateComplete;
const telemetrySentPromise = new Promise(resolve => {
heartbeat.eventEmitter.once("TelemetrySent", payload =>
resolve(payload)
);
});
// This method would be triggered when the timer timed out. This will
// trigger telemetry to be submitted.
heartbeat.close();
const payload = await telemetrySentPromise;
const result = JsonSchema.validate(payload, PAYLOAD_SCHEMA);
Assert.ok(result.valid);
Assert.equal(payload.surveyVersion, "1");
doEnrollmentCleanup();
} finally {
ShowHeartbeatAction.overrideHeartbeatForTests();
sandbox.restore();
}
}
);