commit 75e4233accc8a8acb5c778cd104b27bbf50280ec
parent c2b2eab6f2932e847772bc1036a762b50a7b82cb
Author: Meg Viar <lmegviar@gmail.com>
Date: Tue, 28 Oct 2025 12:55:25 +0000
Bug 1994786 - Add additional tests for TOU and legacy telemetry reporting policy notification flows that confirm the correct state of canUpload in various scenarios r=toolkit-telemetry-reviewers,dmose,TravisLong
The "truth table" in this new test file is based on the manual QA steps outlined in D268883 (see below).
**Overview**
For canUpload to be true, the user must:
- have "datareporting.policy.dataSubmissionEnabled" set to true
- not qualify to be shown the Terms of Use Preonboarding Modal and should (see A below)
- also not qualify to be shown the Legacy Data-Reporting Notification Flow (see B below)
**A) A user do NOT qualify to see the TOU Preonboarding Modal unless ONE OR MORE of the following are true:**
- Preonboarding is disabled or no screens are configured
- Preonboading is enabled by default on all OSs EXCEPT Linux. You can set preonbarding as enabled on Linux by setting “browser.preonboarding.enabled” to true.
- When preonboarding is enabled, screens are configured by default
- The user was already notified via the legacy data-reporting flow
- "datareporting.policy.dataSubmissionPolicyAcceptedVersion" is set to 1 or higher and “datareporting.policy.dataSubmissionPolicyNotifiedTime” is set to a timestamp (you can use current date/time).
- ‘termsofuse.bypassNotification’ pref is set to true
- The user has already accepted the Terms of Use
- “termsouse.acceptedVersion” is set to 4 or higher
- “termsofuse.acceptedDate” is set to a timestamp (you can use current date/time).
- A notification is current in progress
- For example, if the TOU modal is actively showing on first startup and you’ve yet to click the accept button.
**B) A user do NOT qualify to see the Legacy Data-Reporting Notification Flow unless ONE OR MORE of the following are true:**
//Note that items in italics are already checked in the list to determine if users qualify to see the Terms of Use modal above.//
- "datareporting.policy.dataSubmissionPolicyBypassNotification" pref is set to true
- //The user has already accepted the Terms of Use//
- //“termsouse.acceptedVersion” is set to 4 or higher//
- //“termsofuse.acceptedDate” is set to a timestamp (you can use current date/time).//
- //The user was already notified via the legacy data-reporting flow//
- //"datareporting.policy.dataSubmissionPolicyAcceptedVersion" is set to 1 or higher//
- //“datareporting.policy.dataSubmissionPolicyNotifiedTime” is set to a timestamp (you can use current date/time).//
- //A notification is current in progress//
- //For example, if the TOU modal is actively showing on first startup and you’ve yet to click the accept button.//
**Manual Testing**
You can see the value of canUpload by entering the following in the browser toolbox console:
```
const { TelemetryReportingPolicy } = ChromeUtils.importESModule("resource://gre/modules/TelemetryReportingPolicy.sys.mjs”);
TelemetryReportingPolicy.canUpload()
```
- If `"datareporting.policy.dataSubmissionEnabled" is set to false, canUpload should ALWAYS be FALSE.
- If one or more conditions from list A AND one or more conditions from list B above are met, then canUpload should be TRUE.
- If NO conditions from list A are met OR if NO conditions from list B are met, then canUpload should be FALSE.
Differential Revision: https://phabricator.services.mozilla.com/D269628
Diffstat:
3 files changed, 468 insertions(+), 1 deletion(-)
diff --git a/toolkit/components/telemetry/app/TelemetryReportingPolicy.sys.mjs b/toolkit/components/telemetry/app/TelemetryReportingPolicy.sys.mjs
@@ -141,7 +141,8 @@ export var TelemetryReportingPolicy = {
* 2. The user is not eligible to see the ToU. Example local builds and temporarily Linux.
*/
TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE: "telemetry-tou-accepted-or-ineligible",
-
+ // Make this value accessible on TelemetryReportingPolicy
+ OLDEST_ALLOWED_TOU_ACCEPTANCE_YEAR,
/**
* Setup the policy.
*/
diff --git a/toolkit/components/telemetry/tests/unit/test_canUpload_truth_table.js b/toolkit/components/telemetry/tests/unit/test_canUpload_truth_table.js
@@ -0,0 +1,458 @@
+/* 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";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+});
+
+const { NimbusTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/NimbusTestUtils.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+const { Policy, TelemetryReportingPolicy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+);
+
+const PREONBOARDING_ENABLED_PREF = "browser.preonboarding.enabled";
+const TOU_ACCEPTED_VERSION_PREF = "termsofuse.acceptedVersion";
+const TOU_ACCEPTED_DATE_PREF = "termsofuse.acceptedDate";
+const TOU_MINIMUM_VERSION_PREF = "termsofuse.minimumVersion";
+const TOU_CURRENT_VERSION_PREF = "termsofuse.currentVersion";
+const TOU_BYPASS_NOTIFICATION_PREF = "termsofuse.bypassNotification";
+const TOU_PREF_MIGRATION_CHECK = "browser.termsofuse.prefMigrationCheck";
+
+const CURRENT_VERSION = 900;
+const MINIMUM_VERSION = 899;
+NimbusTestUtils.init(this);
+
+add_setup(async function common_setup() {
+ // Initialize the addon test harness before startup.
+ // Use `globalThis` as 'this' can be undefined on Linux.
+ AddonTestUtils.init(globalThis);
+ // Ensure we have a real profile before we start the Addon Manager.
+ do_get_profile();
+ // Avoid cert flakiness in tests.
+ AddonTestUtils.overrideCertDB();
+ // Ensure AddonManager is initialized before anything tries to observe it.
+ AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1.0",
+ "1.0"
+ );
+ await AddonTestUtils.promiseStartupManager();
+
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ false
+ );
+ Services.prefs.setBoolPref(TOU_BYPASS_NOTIFICATION_PREF, false);
+
+ TelemetryReportingPolicy.setup();
+
+ const { cleanup } = await NimbusTestUtils.setupTest();
+ registerCleanupFunction(cleanup);
+
+ registerCleanupFunction(async () => {
+ for (const pref of [
+ PREONBOARDING_ENABLED_PREF,
+ TOU_BYPASS_NOTIFICATION_PREF,
+ TOU_ACCEPTED_DATE_PREF,
+ TOU_ACCEPTED_VERSION_PREF,
+ TOU_MINIMUM_VERSION_PREF,
+ TOU_CURRENT_VERSION_PREF,
+ TOU_PREF_MIGRATION_CHECK,
+ TelemetryUtils.Preferences.BypassNotification,
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ TelemetryUtils.Preferences.AcceptedPolicyDate,
+ TelemetryUtils.Preferences.DataSubmissionEnabled,
+ ]) {
+ Services.prefs.clearUserPref(pref);
+ }
+ TelemetryReportingPolicy.testNotificationInProgress(false);
+ TelemetryReportingPolicy.reset();
+ await AddonTestUtils.promiseShutdownManager();
+ sinon.restore();
+ });
+});
+
+async function enrollPreonboarding({
+ enabled = true,
+ currentVersion,
+ minimumVersion,
+}) {
+ Services.prefs.setIntPref(TOU_CURRENT_VERSION_PREF, currentVersion);
+ Services.prefs.setIntPref(TOU_MINIMUM_VERSION_PREF, minimumVersion);
+
+ return NimbusTestUtils.enrollWithFeatureConfig(
+ {
+ featureId: NimbusFeatures.preonboarding.featureId,
+ value: {
+ enabled,
+ currentVersion,
+ minimumVersion,
+ screens: [{ id: "test" }],
+ },
+ },
+ { isRollout: false }
+ );
+}
+
+function setDataSubmissionEnabled(on) {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.DataSubmissionEnabled,
+ !!on
+ );
+}
+
+/**
+ * Mutators are per-row helpers that set up state for either the Terms of Use
+ * (“A”) path or the legacy data reporting notification (“B”) path.
+ *
+ * Constraints:
+ * - Each mutator only touches the specific prefs or policy flags named in the
+ * function and no unrelated global state.
+ * - The test clears all related prefs before each row, ensuring a clean
+ * baseline. Each mutator must therefore set every value it needs so rows are
+ * independent and can run in any order without hidden dependencies.
+ *
+ * Usage:
+ * - ROWS.A and ROWS.B contain arrays of mutator names. The test runner calls
+ * each in sequence to produce the preconditions for that row.
+ */
+const Mutators = {
+ // A cases - TOU does NOT qualify to show
+ A_touDisabled() {
+ Services.prefs.setBoolPref(PREONBOARDING_ENABLED_PREF, false);
+ },
+ A_touBypass() {
+ Services.prefs.setBoolPref(TOU_BYPASS_NOTIFICATION_PREF, true);
+ },
+ A_touBypassFalse() {
+ Services.prefs.setBoolPref(TOU_BYPASS_NOTIFICATION_PREF, false);
+ },
+ A_touAccepted() {
+ Services.prefs.setStringPref(TOU_ACCEPTED_DATE_PREF, String(Date.now()));
+ const current = Services.prefs.getIntPref(
+ TOU_CURRENT_VERSION_PREF,
+ CURRENT_VERSION
+ );
+ Services.prefs.setIntPref(TOU_ACCEPTED_VERSION_PREF, current);
+ },
+ A_touNotAccepted() {
+ Services.prefs.clearUserPref(TOU_ACCEPTED_DATE_PREF);
+ Services.prefs.clearUserPref(TOU_ACCEPTED_VERSION_PREF);
+ },
+ A_touAcceptedOld() {
+ // Choose a date that is guaranteed to be rejected by setting one year
+ // before minimum.
+ const old = new Date(
+ `${Policy.OLDEST_ALLOWED_TOU_ACCEPTANCE_YEAR - 1}-01-01T00:00:00Z`
+ ).getTime(); // older than allowed
+ Services.prefs.setStringPref(TOU_ACCEPTED_DATE_PREF, String(old));
+ const current = Services.prefs.getIntPref(
+ TOU_CURRENT_VERSION_PREF,
+ CURRENT_VERSION
+ );
+ Services.prefs.setIntPref(TOU_ACCEPTED_VERSION_PREF, current);
+ },
+ A_touAcceptedBelowMin() {
+ const min = Services.prefs.getIntPref(
+ TOU_MINIMUM_VERSION_PREF,
+ MINIMUM_VERSION
+ );
+ Services.prefs.setStringPref(TOU_ACCEPTED_DATE_PREF, String(Date.now()));
+ Services.prefs.setIntPref(TOU_ACCEPTED_VERSION_PREF, Math.max(0, min - 1));
+ },
+
+ // B cases - legacy flow does NOT qualify to show
+ B_legacyBypass() {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ true
+ );
+ },
+ B_legacyBypassFalse() {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ false
+ );
+ },
+ B_legacyNotified() {
+ const min = Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.MinimumPolicyVersion,
+ 1
+ );
+ Services.prefs.setIntPref(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ min
+ );
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.AcceptedPolicyDate,
+ String(Date.now())
+ );
+ },
+ B_legacyNotifiedFalse() {
+ Services.prefs.clearUserPref(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion
+ );
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.AcceptedPolicyDate);
+ },
+ // Users cannot upload if a notification is in progress
+ inProgressSetTrue() {
+ TelemetryReportingPolicy.testNotificationInProgress(true);
+ },
+ inProgressSetFalse() {
+ TelemetryReportingPolicy.testNotificationInProgress(false);
+ },
+};
+
+/**
+ * Truth table
+ *
+ * Fields:
+ * - name: string description for logging
+ * - submissionEnabled: boolean mapped to
+ * TelemetryUtils.Preferences.DataSubmissionEnabled
+ * - A: array<string> of “A_” mutators (TOU path), formatted as strings for easy
+ * logging
+ * - B: array<string> of “B_” mutators (legacy path), formatted as strings for
+ * easy logging
+ * - inProgress: "tou" | "legacy" | null If set, we force
+ * TelemetryReportingPolicy.testNotificationInProgress(true) to model a
+ * notification (Terms of User or Legacy) currently showing.
+ * - preNimbusEvaluate: If true, call canUpload() once before Nimbus variables
+ * are populated, then again after we simulate startup, triggering
+ * _delayedStartup. We expect both results to match. This ensures that even if
+ * canUpload() is called before the Nimbus Variables are evaluated as part of
+ * the _delayedStartup process, we evaluate them on the fly so that their
+ * value is consistent.
+ * - expect: boolean expected final result of canUpload()
+ * - expectMigration: if true, we assert that the legacy to TOU pref migration
+ * ran
+ */
+const ROWS = [
+ {
+ name: "Data submission disable -> false",
+ submissionEnabled: false,
+ A: ["A_touNotAccepted", "A_touBypassFalse"],
+ B: ["B_legacyBypassFalse", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: false,
+ },
+ {
+ name: "TOU bypass and Legacy bypass -> true",
+ submissionEnabled: true,
+ A: ["A_touBypass", "A_touNotAccepted"],
+ B: ["B_legacyBypass", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: true,
+ },
+ {
+ name: "TOU accepted and Legacy notified -> true",
+ submissionEnabled: true,
+ A: ["A_touAccepted", "A_touBypassFalse"],
+ B: ["B_legacyNotified", "B_legacyBypassFalse"],
+ inProgress: null,
+ expect: true,
+ },
+ {
+ name: "TOU bypass only -> false",
+ submissionEnabled: true,
+ A: ["A_touBypass", "A_touNotAccepted"],
+ B: ["B_legacyBypassFalse", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: false,
+ },
+ {
+ name: "Legacy bypass only -> true",
+ submissionEnabled: true,
+ A: ["A_touNotAccepted", "A_touBypassFalse"],
+ B: ["B_legacyBypass", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: true,
+ // Pref migration will result in legacy bypass value being mirrored in TOU
+ // bypass value
+ expectMigration: true,
+ },
+ {
+ name: "Legacy notified -> true",
+ submissionEnabled: true,
+ A: ["A_touNotAccepted", "A_touBypassFalse"],
+ B: ["B_legacyNotified", "B_legacyBypassFalse"],
+ inProgress: null,
+ expect: true,
+ },
+ {
+ name: "TOU notification in progress -> false",
+ submissionEnabled: true,
+ A: ["A_touBypassFalse", "A_touNotAccepted"],
+ B: ["B_legacyBypassFalse", "B_legacyNotifiedFalse"],
+ inProgress: "tou",
+ expect: false,
+ },
+ {
+ name: "Legacy notification in progress -> false",
+ submissionEnabled: true,
+ A: ["A_touNotAccepted", "A_touBypassFalse"],
+ B: ["B_legacyBypassFalse", "B_legacyNotifiedFalse"],
+ inProgress: "legacy",
+ expect: false,
+ },
+ {
+ name: "TOU accepted with invalid date, not notified of legacy flow -> false",
+ submissionEnabled: true,
+ A: ["A_touAcceptedOld", "A_touBypassFalse"],
+ B: ["B_legacyBypassFalse", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: false,
+ },
+ {
+ name: "TOU accepted below minimum version, not notified of legacy flow -> false",
+ submissionEnabled: true,
+ A: ["A_touAcceptedBelowMin", "A_touBypassFalse"],
+ B: ["B_legacyBypassFalse", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: false,
+ },
+ {
+ name: "TOU accepted with invalid date, Legacy bypass -> false",
+ submissionEnabled: true,
+ A: ["A_touAcceptedOld", "A_touBypassFalse"],
+ B: ["B_legacyBypass", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: false,
+ },
+ {
+ name: "TOU accepted below minimum version, Legacy bypass -> false",
+ submissionEnabled: true,
+ A: ["A_touAcceptedBelowMin", "A_touBypassFalse"],
+ B: ["B_legacyBypass", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: false,
+ },
+ {
+ name: "Nimbus value for preonboarding enabled settles to the same value regardless of when called -> true",
+ submissionEnabled: true,
+ preNimbusEvaluate: true,
+ A: ["A_touDisabled", "A_touBypassFalse", "A_touNotAccepted"],
+ B: ["B_legacyBypass", "B_legacyNotifiedFalse"],
+ inProgress: null,
+ expect: true,
+ // Pref migration will result in legacy bypass value being mirrored in TOU
+ // bypass value
+ expectMigration: true,
+ },
+];
+
+/**
+ * Tests TelemetryReportingPolicy.canUpload() using a declarative truth table.
+ *
+ * Each row defines the relevant prefs and policy state (TOU, legacy,
+ * in-progress) to verify that canUpload() returns the expected result across
+ * combinations.
+ */
+add_task(async function test_canUpload_truth_table() {
+ const unenroll = await enrollPreonboarding({
+ enabled: true,
+ currentVersion: CURRENT_VERSION,
+ minimumVersion: MINIMUM_VERSION,
+ });
+ registerCleanupFunction(unenroll);
+
+ for (const row of ROWS) {
+ info(`ROW: ${row.name}`);
+ TelemetryReportingPolicy.reset();
+ sinon.restore();
+ const modalStub = sinon.stub(Policy, "showModal").returns(true);
+
+ for (const pref of [
+ PREONBOARDING_ENABLED_PREF,
+ TOU_BYPASS_NOTIFICATION_PREF,
+ TOU_ACCEPTED_DATE_PREF,
+ TOU_ACCEPTED_VERSION_PREF,
+ TOU_PREF_MIGRATION_CHECK,
+ TelemetryUtils.Preferences.BypassNotification,
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ TelemetryUtils.Preferences.AcceptedPolicyDate,
+ TelemetryUtils.Preferences.DataSubmissionEnabled,
+ ]) {
+ Services.prefs.clearUserPref(pref);
+ }
+ // Normalize browser.preonboarding.enabled across platforms (currently set
+ // to "false" by default on Linux).
+ Services.prefs.setBoolPref(PREONBOARDING_ENABLED_PREF, true);
+
+ Mutators.inProgressSetFalse();
+
+ setDataSubmissionEnabled(!!row.submissionEnabled);
+
+ // Some callers may evaluate canUpload() before Nimbus variables are
+ // initialized. The `preNimbusEvaluate` flag triggers an early call to
+ // canUpload() before fakeSessionRestoreNotification(), then checks
+ // afterward that the result "converges" once Nimbus state is finalized.
+ // This ensures early evaluations don't leave inconsistent or stale policy
+ // state.
+ if (row.preNimbusEvaluate) {
+ const pre = TelemetryReportingPolicy.canUpload();
+ info(`Before Nimbus initialization, canUpload() = ${pre}`);
+ }
+
+ // Apply conditions
+ for (const m of row.A) {
+ Mutators[m]();
+ }
+ for (const m of row.B) {
+ Mutators[m]();
+ }
+
+ await Policy.fakeSessionRestoreNotification();
+
+ if (row.expectMigration) {
+ Assert.ok(
+ Services.prefs.getBoolPref(TOU_PREF_MIGRATION_CHECK, false),
+ "TOU pref migration ran"
+ );
+ }
+
+ // Force “in progress” if requested
+ if (row.inProgress) {
+ Mutators.inProgressSetTrue();
+ } else {
+ Mutators.inProgressSetFalse();
+ }
+
+ const got = TelemetryReportingPolicy.canUpload();
+ Assert.equal(
+ got,
+ row.expect,
+ `canUpload() matches expectation for: ${row.name}`
+ );
+
+ // This checks to ensure that that even if canUpload() is called before the
+ // Nimbus Variables are evaluated as part of the _delayedStartup process,
+ // we evaluate them on the fly so that their value is consistent.
+ if (row.preNimbusEvaluate) {
+ const after = TelemetryReportingPolicy.canUpload();
+ Assert.equal(
+ after,
+ row.expect,
+ `After Nimbus initialization, canUpload() converged for: ${row.name}`
+ );
+ }
+
+ // Per-row teardown
+ modalStub.restore();
+ Mutators.inProgressSetFalse();
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/xpcshell.toml b/toolkit/components/telemetry/tests/unit/xpcshell.toml
@@ -161,6 +161,14 @@ skip-if = [
["test_UtilityScalars.js"]
run-if = ["os == 'win'"]
+["test_canUpload_truth_table.js"]
+skip-if = [
+ "appname == 'thunderbird'",
+ "os == 'android'",
+]
+# The policy notification/acceptance flows are desktop only and not used by
+# Thunderbird or Android.
+
["test_client_id.js"]
["test_failover_retry.js"]