tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d6f9ff3ebcdbb2ef0ea043a5897c680c6845fec3
parent 0a4067b8b3e0bc3b297c83348aa71940ed2c4a65
Author: Meg Viar <lmegviar@gmail.com>
Date:   Fri, 24 Oct 2025 18:48:51 +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:
Mtoolkit/components/telemetry/app/TelemetryReportingPolicy.sys.mjs | 3++-
Atoolkit/components/telemetry/tests/unit/test_canUpload_truth_table.js | 440+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/telemetry/tests/unit/xpcshell.toml | 8++++++++
3 files changed, 450 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,440 @@ +/* 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", +}); + +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() { + 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(() => { + 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(); + 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"]