summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js')
-rw-r--r--toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js514
1 files changed, 514 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js b/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js
new file mode 100644
index 0000000000..4a6cee3b43
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js
@@ -0,0 +1,514 @@
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+const { getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+const { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
+ "resource://gre/modules/FirefoxRelayTelemetry.mjs"
+);
+
+const gFxAccounts = getFxAccountsSingleton();
+let gRelayACOptionsTitles;
+let gHttpServer;
+
+const TEST_URL_PATH = `https://example.org${DIRECTORY_PATH}form_basic_signup.html`;
+
+const MOCK_MASKS = [
+ {
+ full_address: "email1@mozilla.com",
+ description: "Email 1 Description",
+ enabled: true,
+ },
+ {
+ full_address: "email2@mozilla.com",
+ description: "Email 2 Description",
+ enabled: false,
+ },
+ {
+ full_address: "email3@mozilla.com",
+ description: "Email 3 Description",
+ enabled: true,
+ },
+];
+
+const SERVER_SCENARIOS = {
+ free_tier_limit: {
+ "/relayaddresses/": {
+ POST: (request, response) => {
+ response.setStatusLine(request.httpVersion, 403);
+ response.write(JSON.stringify({ error_code: "free_tier_limit" }));
+ },
+ GET: (_, response) => {
+ response.write(JSON.stringify(MOCK_MASKS));
+ },
+ },
+ },
+ unknown_error: {
+ "/relayaddresses/": {
+ default: (request, response) => {
+ response.setStatusLine(request.httpVersion, 408);
+ },
+ },
+ },
+
+ default: {
+ default: (request, response) => {
+ response.setStatusLine(request.httpVersion, 200);
+ response.write(JSON.stringify({ foo: "bar" }));
+ },
+ },
+};
+
+const simpleRouter = scenarioName => (request, response) => {
+ const routeHandler =
+ SERVER_SCENARIOS[scenarioName][request._path] ?? SERVER_SCENARIOS.default;
+ const methodHandler =
+ routeHandler?.[request._method] ??
+ routeHandler.default ??
+ SERVER_SCENARIOS.default.default;
+ methodHandler(request, response);
+};
+const setupServerScenario = (scenarioName = "default") =>
+ gHttpServer.registerPrefixHandler("/", simpleRouter(scenarioName));
+
+const setupRelayScenario = async scenarioName => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.firefoxRelay.feature", scenarioName]],
+ });
+ Services.telemetry.clearEvents();
+};
+
+const waitForEvents = async expectedEvents =>
+ TestUtils.waitForCondition(
+ () => {
+ const snapshots = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+
+ return (snapshots.parent?.length ?? 0) >= (expectedEvents.length ?? 0);
+ },
+ "Wait for telemetry to be collected",
+ 100,
+ 100
+ );
+
+async function assertEvents(expectedEvents) {
+ // To avoid intermittent failures, we wait for telemetry to be collected
+ await waitForEvents(expectedEvents);
+ const events = TelemetryTestUtils.getEvents(
+ { category: "relay_integration" },
+ { process: "parent" }
+ );
+
+ for (let i = 0; i < expectedEvents.length; i++) {
+ const keysInExpectedEvent = Object.keys(expectedEvents[i]);
+ keysInExpectedEvent.forEach(key => {
+ const assertFn =
+ typeof events[i][key] === "object"
+ ? Assert.deepEqual.bind(Assert)
+ : Assert.equal.bind(Assert);
+ assertFn(
+ events[i][key],
+ expectedEvents[i][key],
+ `Key value for ${key} should match`
+ );
+ });
+ }
+}
+
+async function openRelayAC(browser) {
+ // In rare cases, especially in chaos mode in verify tests, some events creep in.
+ // Clear them out before we start.
+ Services.telemetry.clearEvents();
+ const popup = document.getElementById("PopupAutoComplete");
+ await openACPopup(popup, browser, "#form-basic-username");
+ const popupItem = document
+ .querySelector("richlistitem")
+ .getAttribute("ac-label");
+ const popupItemTitle = JSON.parse(popupItem).title;
+
+ Assert.ok(
+ gRelayACOptionsTitles.some(title => title.value === popupItemTitle),
+ "AC Popup has an item Relay option shown in popup"
+ );
+
+ const promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.firstChild.getItemAtIndex(0).click();
+ await promiseHidden;
+}
+
+add_setup(async function () {
+ gHttpServer = new HttpServer();
+ setupServerScenario();
+
+ gHttpServer.start(-1);
+
+ const API_ENDPOINT = `http://localhost:${gHttpServer.identity.primaryPort}/`;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["signon.firefoxRelay.feature", "available"],
+ ["signon.firefoxRelay.base_url", API_ENDPOINT],
+ ],
+ });
+
+ sinon.stub(gFxAccounts, "hasLocalSession").returns(true);
+ sinon
+ .stub(gFxAccounts.constructor.config, "isProductionConfig")
+ .returns(true);
+ sinon.stub(gFxAccounts, "getOAuthToken").returns("MOCK_TOKEN");
+ sinon.stub(gFxAccounts, "getSignedInUser").returns({
+ email: "example@mozilla.com",
+ });
+
+ const canRecordExtendedOld = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.telemetry.clearEvents();
+ Services.telemetry.setEventRecordingEnabled("relay_integration", true);
+
+ gRelayACOptionsTitles = await new Localization([
+ "browser/firefoxRelay.ftl",
+ "toolkit/branding/brandings.ftl",
+ ]).formatMessages([
+ "firefox-relay-opt-in-title-1",
+ "firefox-relay-use-mask-title",
+ ]);
+
+ registerCleanupFunction(async () => {
+ await new Promise(resolve => {
+ gHttpServer.stop(function () {
+ resolve();
+ });
+ });
+ Services.telemetry.setEventRecordingEnabled("relay_integration", false);
+ Services.telemetry.clearEvents();
+ Services.telemetry.canRecordExtended = canRecordExtendedOld;
+ sinon.restore();
+ });
+});
+
+add_task(async function test_pref_toggle() {
+ await setupRelayScenario("available");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:preferences#privacy",
+ },
+ async function (browser) {
+ const relayIntegrationCheckbox = content.document.querySelector(
+ "checkbox#relayIntegration"
+ );
+ relayIntegrationCheckbox.click();
+ relayIntegrationCheckbox.click();
+ await assertEvents([
+ { object: "pref_change", method: "disabled" },
+ { object: "pref_change", method: "enabled" },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_popup_option_optin_enabled() {
+ await setupRelayScenario("available");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+
+ notificationPopup
+ .querySelector("button.popup-notification-primary-button")
+ .click();
+
+ await notificationHidden;
+
+ await BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popuphidden"
+ );
+
+ await assertEvents([
+ {
+ object: "offer_relay",
+ method: "shown",
+ extra: { is_relay_user: "true", scenario: "SignUpFormScenario" },
+ },
+ {
+ object: "offer_relay",
+ method: "clicked",
+ extra: { is_relay_user: "true", scenario: "SignUpFormScenario" },
+ },
+ { object: "opt_in_panel", method: "shown" },
+ { object: "opt_in_panel", method: "enabled" },
+ {
+ object: "fill_username",
+ method: "shown",
+ extra: { error_code: "0" },
+ },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_popup_option_optin_postponed() {
+ await setupRelayScenario("available");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+
+ notificationPopup
+ .querySelector("button.popup-notification-secondary-button")
+ .click();
+
+ await notificationHidden;
+
+ await assertEvents([
+ { object: "offer_relay", method: "shown" },
+ { object: "offer_relay", method: "clicked" },
+ { object: "opt_in_panel", method: "shown" },
+ { object: "opt_in_panel", method: "postponed" },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_popup_option_optin_disabled() {
+ await setupRelayScenario("available");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+ const menupopup = notificationPopup.querySelector("menupopup");
+ const menuitem = menupopup.querySelector("menuitem");
+
+ menuitem.click();
+ await notificationHidden;
+
+ await assertEvents([
+ { object: "offer_relay", method: "shown" },
+ { object: "offer_relay", method: "clicked" },
+ { object: "opt_in_panel", method: "shown" },
+ { object: "opt_in_panel", method: "disabled" },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_popup_option_fillusername() {
+ await setupRelayScenario("enabled");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ await BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popuphidden"
+ );
+ await assertEvents([
+ { object: "fill_username", method: "shown" },
+ {
+ object: "fill_username",
+ method: "clicked",
+ },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_fillusername_free_tier_limit() {
+ await setupRelayScenario("enabled");
+ setupServerScenario("free_tier_limit");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+ notificationPopup.querySelector(".reusable-relay-masks button").click();
+ await notificationHidden;
+
+ await assertEvents([
+ { object: "fill_username", method: "shown" },
+ {
+ object: "fill_username",
+ method: "clicked",
+ },
+ {
+ object: "fill_username",
+ method: "shown",
+ extra: { error_code: "free_tier_limit" },
+ },
+ {
+ object: "reuse_panel",
+ method: "shown",
+ },
+ {
+ object: "reuse_panel",
+ method: "reuse_mask",
+ },
+ ]);
+
+ await SpecialPowers.spawn(browser, [], async function () {
+ const username = content.document.getElementById("form-basic-username");
+ Assert.equal(
+ username.value,
+ "email1@mozilla.com",
+ "Username field should be filled with the first mask"
+ );
+ });
+ }
+ );
+});
+
+add_task(async function test_fillusername_error() {
+ await setupRelayScenario("enabled");
+ setupServerScenario("unknown_error");
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+
+ await notificationShown;
+ Assert.equal(
+ notificationPopup.querySelector("popupnotification").id,
+ "relay-integration-error-notification",
+ "Error message should be displayed"
+ );
+
+ await assertEvents([
+ { object: "fill_username", method: "shown" },
+ {
+ object: "fill_username",
+ method: "clicked",
+ },
+ {
+ object: "reuse_panel",
+ method: "shown",
+ extra: { error_code: "408" },
+ },
+ ]);
+ }
+ );
+});
+
+add_task(async function test_auth_token_error() {
+ setupRelayScenario("enabled");
+ gFxAccounts.getOAuthToken.restore();
+ const oauthTokenStub = sinon.stub(gFxAccounts, "getOAuthToken").throws();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ await openRelayAC(browser);
+ const notificationPopup = document.getElementById("notification-popup");
+ const notificationShown = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "shown"
+ );
+ const notificationHidden = BrowserTestUtils.waitForPopupEvent(
+ notificationPopup,
+ "hidden"
+ );
+
+ await notificationShown;
+
+ notificationPopup
+ .querySelector("button.popup-notification-primary-button")
+ .click();
+
+ await notificationHidden;
+
+ await assertEvents([
+ {
+ object: "fill_username",
+ method: "shown",
+ extra: { error_code: "0" },
+ },
+ {
+ object: "fill_username",
+ method: "clicked",
+ extra: { error_code: "0" },
+ },
+ {
+ object: "fill_username",
+ method: "shown",
+ extra: { error_code: "418" },
+ },
+ ]);
+ }
+ );
+ oauthTokenStub.restore();
+});