summaryrefslogtreecommitdiffstats
path: root/browser/components/doh/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/doh/test')
-rw-r--r--browser/components/doh/test/browser/browser.toml33
-rw-r--r--browser/components/doh/test/browser/browser_cleanFlow.js88
-rw-r--r--browser/components/doh/test/browser/browser_dirtyEnable.js55
-rw-r--r--browser/components/doh/test/browser/browser_doh_region.js60
-rw-r--r--browser/components/doh/test/browser/browser_doorhangerUserReject.js90
-rw-r--r--browser/components/doh/test/browser/browser_platformDetection.js126
-rw-r--r--browser/components/doh/test/browser/browser_policyOverride.js76
-rw-r--r--browser/components/doh/test/browser/browser_providerSteering.js120
-rw-r--r--browser/components/doh/test/browser/browser_remoteSettings_newProfile.js147
-rw-r--r--browser/components/doh/test/browser/browser_remoteSettings_rollout.js70
-rw-r--r--browser/components/doh/test/browser/browser_rollback.js144
-rw-r--r--browser/components/doh/test/browser/browser_throttle_heuristics.js97
-rw-r--r--browser/components/doh/test/browser/browser_trrSelect.js147
-rw-r--r--browser/components/doh/test/browser/browser_trrSelection_disable.js74
-rw-r--r--browser/components/doh/test/browser/browser_userInterference.js81
-rw-r--r--browser/components/doh/test/browser/head.js408
-rw-r--r--browser/components/doh/test/unit/head.js99
-rw-r--r--browser/components/doh/test/unit/test_DNSLookup.js62
-rw-r--r--browser/components/doh/test/unit/test_LookupAggregator.js162
-rw-r--r--browser/components/doh/test/unit/test_TRRRacer.js209
-rw-r--r--browser/components/doh/test/unit/test_heuristics.js80
-rw-r--r--browser/components/doh/test/unit/xpcshell.toml14
22 files changed, 2442 insertions, 0 deletions
diff --git a/browser/components/doh/test/browser/browser.toml b/browser/components/doh/test/browser/browser.toml
new file mode 100644
index 0000000000..0cf2d4937b
--- /dev/null
+++ b/browser/components/doh/test/browser/browser.toml
@@ -0,0 +1,33 @@
+[DEFAULT]
+head = "head.js"
+
+["browser_cleanFlow.js"]
+skip-if = ["socketprocess_networking"]
+
+["browser_dirtyEnable.js"]
+
+["browser_doh_region.js"]
+
+["browser_doorhangerUserReject.js"]
+
+["browser_platformDetection.js"]
+
+["browser_policyOverride.js"]
+
+["browser_providerSteering.js"]
+
+["browser_remoteSettings_newProfile.js"]
+skip-if = ["win11_2009 && bits == 32"] # Bug 1713464
+
+["browser_remoteSettings_rollout.js"]
+skip-if = ["win11_2009 && bits == 32"] # Bug 1713464
+
+["browser_rollback.js"]
+
+["browser_throttle_heuristics.js"]
+
+["browser_trrSelect.js"]
+
+["browser_trrSelection_disable.js"]
+
+["browser_userInterference.js"]
diff --git a/browser/components/doh/test/browser/browser_cleanFlow.js b/browser/components/doh/test/browser/browser_cleanFlow.js
new file mode 100644
index 0000000000..9beb1a5a26
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_cleanFlow.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+add_task(async function testCleanFlow() {
+ // Set up a passing environment and enable DoH.
+ setPassingHeuristics();
+ let promise = waitForDoorhanger();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
+ Preferences.set(prefs.ENABLED_PREF, true);
+
+ await prefPromise;
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/dns-query",
+ "TRR selection complete."
+ );
+ await checkTRRSelectionTelemetry();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, EXAMPLE_URL);
+ let panel = await promise;
+
+ prefPromise = TestUtils.waitForPrefChange(
+ prefs.DOORHANGER_USER_DECISION_PREF
+ );
+
+ // Click the doorhanger's "accept" button.
+ let button = panel.querySelector(".popup-notification-primary-button");
+ promise = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await promise;
+
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ await prefPromise;
+ is(
+ Preferences.get(prefs.DOORHANGER_USER_DECISION_PREF),
+ "UIOk",
+ "Doorhanger decision saved."
+ );
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb not cleared.");
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Change the environment to failing and simulate a network change.
+ setFailingHeuristics();
+ simulateNetworkChange();
+ await ensureTRRMode(0);
+ await checkHeuristicsTelemetry("disable_doh", "netchange");
+
+ // Trigger another network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(0);
+ await checkHeuristicsTelemetry("disable_doh", "netchange");
+
+ // Restart the controller for good measure.
+ await restartDoHController();
+ ensureNoTRRSelectionTelemetry();
+ // The mode technically changes from undefined/empty to 0 here.
+ await ensureTRRMode(0);
+ await checkHeuristicsTelemetry("disable_doh", "startup");
+
+ // Set a passing environment and simulate a network change.
+ setPassingHeuristics();
+ simulateNetworkChange();
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "netchange");
+
+ // Again, repeat and check nothing changed.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(2);
+ await checkHeuristicsTelemetry("enable_doh", "netchange");
+
+ // Test the clearModeOnShutdown pref. `restartDoHController` does the actual
+ // test for us between shutdown and startup.
+ Preferences.set(prefs.CLEAR_ON_SHUTDOWN_PREF, false);
+ await restartDoHController();
+ ensureNoTRRSelectionTelemetry();
+ await ensureNoTRRModeChange(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+ Preferences.set(prefs.CLEAR_ON_SHUTDOWN_PREF, true);
+});
diff --git a/browser/components/doh/test/browser/browser_dirtyEnable.js b/browser/components/doh/test/browser/browser_dirtyEnable.js
new file mode 100644
index 0000000000..c704ca06e6
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_dirtyEnable.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+add_task(async function testDirtyEnable() {
+ // Set up a failing environment, pre-set DoH to enabled, and verify that
+ // when the add-on is enabled, it doesn't do anything - DoH remains turned on.
+ setFailingHeuristics();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.DISABLED_PREF);
+ Preferences.set(prefs.NETWORK_TRR_MODE_PREF, 2);
+ Preferences.set(prefs.ENABLED_PREF, true);
+ await prefPromise;
+ is(
+ Preferences.get(prefs.DISABLED_PREF, false),
+ true,
+ "Disabled state recorded."
+ );
+ is(
+ Preferences.get(prefs.BREADCRUMB_PREF),
+ undefined,
+ "Breadcrumb not saved."
+ );
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ undefined,
+ "TRR selection not performed."
+ );
+ is(Preferences.get(prefs.NETWORK_TRR_MODE_PREF), 2, "TRR mode preserved.");
+ ensureNoTRRSelectionTelemetry();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+
+ // Simulate a network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+ is(Preferences.get(prefs.NETWORK_TRR_MODE_PREF), 2, "TRR mode preserved.");
+
+ // Restart the controller for good measure.
+ await restartDoHController();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoTRRSelectionTelemetry();
+ ensureNoHeuristicsTelemetry();
+ is(Preferences.get(prefs.NETWORK_TRR_MODE_PREF), 2, "TRR mode preserved.");
+
+ // Simulate a network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ is(Preferences.get(prefs.NETWORK_TRR_MODE_PREF), 2, "TRR mode preserved.");
+ ensureNoHeuristicsTelemetry();
+});
diff --git a/browser/components/doh/test/browser/browser_doh_region.js b/browser/components/doh/test/browser/browser_doh_region.js
new file mode 100644
index 0000000000..56d1ad7142
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_doh_region.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+add_task(async function testPrefFirstRollout() {
+ await setup();
+ await setupRegion();
+ let defaults = Services.prefs.getDefaultBranch("");
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ false,
+ "Rollout should not be enabled"
+ );
+ setPassingHeuristics();
+
+ let configFlushedPromise = DoHTestUtils.waitForConfigFlush();
+ defaults.setBoolPref(`${kRegionalPrefNamespace}.enabled`, true);
+ await configFlushedPromise;
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ true,
+ "Rollout should be enabled"
+ );
+ await ensureTRRMode(2);
+
+ is(
+ Preferences.get("doh-rollout.home-region"),
+ "DE",
+ "Initial region should be DE"
+ );
+ Region._setHomeRegion("UK");
+ await ensureTRRMode(2); // Mode shouldn't change.
+
+ is(Preferences.get("doh-rollout.home-region-changed"), true);
+
+ await DoHController._uninit();
+ await DoHConfigController._uninit();
+
+ // Check after controller gets reinitialized (or restart)
+ // that the region gets set to UK
+ await DoHConfigController.init();
+ await DoHController.init();
+ is(Preferences.get("doh-rollout.home-region"), "UK");
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ false,
+ "Rollout should not be enabled for new region"
+ );
+ await ensureTRRMode(undefined); // restart of the controller should change the region.
+
+ // Reset state to initial values.
+ await setupRegion();
+ defaults.deleteBranch(`doh-rollout.de`);
+ Preferences.reset("doh-rollout.home-region-changed");
+ Preferences.reset("doh-rollout.home-region");
+});
diff --git a/browser/components/doh/test/browser/browser_doorhangerUserReject.js b/browser/components/doh/test/browser/browser_doorhangerUserReject.js
new file mode 100644
index 0000000000..f9468d46cd
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_doorhangerUserReject.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+add_task(async function testDoorhangerUserReject() {
+ // Set up a passing environment and enable DoH.
+ setPassingHeuristics();
+ let promise = waitForDoorhanger();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
+ Preferences.set(prefs.ENABLED_PREF, true);
+
+ await prefPromise;
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/dns-query",
+ "TRR selection complete."
+ );
+ await checkTRRSelectionTelemetry();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, EXAMPLE_URL);
+ let panel = await promise;
+
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ checkScalars([
+ ["networking.doh_heuristics_attempts", { value: 1 }],
+ ["networking.doh_heuristics_pass_count", { value: 1 }],
+ ["networking.doh_heuristics_result", { value: Heuristics.Telemetry.pass }],
+ // All of the heuristics must be false.
+ falseExpectations([]),
+ ]);
+
+ prefPromise = TestUtils.waitForPrefChange(
+ prefs.DOORHANGER_USER_DECISION_PREF
+ );
+
+ // Click the doorhanger's "reject" button.
+ let button = panel.querySelector(".popup-notification-secondary-button");
+ promise = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await promise;
+
+ await prefPromise;
+
+ is(
+ Preferences.get(prefs.DOORHANGER_USER_DECISION_PREF),
+ "UIDisabled",
+ "Doorhanger decision saved."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ await ensureTRRMode(undefined);
+ ensureNoHeuristicsTelemetry();
+ is(Preferences.get(prefs.BREADCRUMB_PREF), undefined, "Breadcrumb cleared.");
+
+ checkScalars([
+ ["networking.doh_heuristics_attempts", { value: 1 }],
+ ["networking.doh_heuristics_pass_count", { value: 1 }],
+ [
+ "networking.doh_heuristics_result",
+ { value: Heuristics.Telemetry.optOut },
+ ],
+ // All of the heuristics must be false.
+ falseExpectations([]),
+ ]);
+
+ // Simulate a network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+
+ // Restart the controller for good measure.
+ await restartDoHController();
+ ensureNoTRRSelectionTelemetry();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+
+ // Set failing environment and trigger another network change.
+ setFailingHeuristics();
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+});
diff --git a/browser/components/doh/test/browser/browser_platformDetection.js b/browser/components/doh/test/browser/browser_platformDetection.js
new file mode 100644
index 0000000000..025546ae9e
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_platformDetection.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Heuristics: "resource:///modules/DoHHeuristics.sys.mjs",
+});
+
+add_task(setup);
+
+add_task(async function testPlatformIndications() {
+ // Check if the platform heuristics actually cause a "disable_doh" event
+ let { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+ );
+
+ let mockedLinkService = {
+ isLinkUp: true,
+ linkStatusKnown: true,
+ linkType: Ci.nsINetworkLinkService.LINK_TYPE_WIFI,
+ networkID: "abcd",
+ dnsSuffixList: [],
+ platformDNSIndications: Ci.nsINetworkLinkService.NONE_DETECTED,
+ QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
+ };
+
+ let networkLinkServiceCID = MockRegistrar.register(
+ "@mozilla.org/network/network-link-service;1",
+ mockedLinkService
+ );
+
+ Heuristics._setMockLinkService(mockedLinkService);
+ registerCleanupFunction(async () => {
+ MockRegistrar.unregister(networkLinkServiceCID);
+ Heuristics._setMockLinkService(undefined);
+ });
+
+ setPassingHeuristics();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
+ Preferences.set(prefs.ENABLED_PREF, true);
+ await prefPromise;
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ checkScalars([
+ ["networking.doh_heuristics_attempts", { value: 1 }],
+ ["networking.doh_heuristics_pass_count", { value: 1 }],
+ ["networking.doh_heuristics_result", { value: Heuristics.Telemetry.pass }],
+ // All of the heuristics must be false.
+ falseExpectations([]),
+ ]);
+
+ await ensureTRRMode(2);
+
+ mockedLinkService.platformDNSIndications =
+ Ci.nsINetworkLinkService.VPN_DETECTED;
+ simulateNetworkChange();
+ await ensureTRRMode(0);
+ await checkHeuristicsTelemetry("disable_doh", "netchange");
+ checkScalars(
+ [
+ ["networking.doh_heuristics_attempts", { value: 2 }],
+ ["networking.doh_heuristics_pass_count", { value: 1 }],
+ ["networking.doh_heuristics_result", { value: Heuristics.Telemetry.vpn }],
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "vpn" }],
+ ].concat(falseExpectations(["vpn"]))
+ );
+
+ mockedLinkService.platformDNSIndications =
+ Ci.nsINetworkLinkService.PROXY_DETECTED;
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(0);
+ await checkHeuristicsTelemetry("disable_doh", "netchange");
+ checkScalars(
+ [
+ ["networking.doh_heuristics_attempts", { value: 3 }],
+ ["networking.doh_heuristics_pass_count", { value: 1 }],
+ [
+ "networking.doh_heuristics_result",
+ { value: Heuristics.Telemetry.proxy },
+ ],
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "vpn" }], // Was tripped earlier this session
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "proxy" }],
+ ].concat(falseExpectations(["vpn", "proxy"]))
+ );
+
+ mockedLinkService.platformDNSIndications =
+ Ci.nsINetworkLinkService.NRPT_DETECTED;
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(0);
+ await checkHeuristicsTelemetry("disable_doh", "netchange");
+ checkScalars(
+ [
+ ["networking.doh_heuristics_attempts", { value: 4 }],
+ ["networking.doh_heuristics_pass_count", { value: 1 }],
+ [
+ "networking.doh_heuristics_result",
+ { value: Heuristics.Telemetry.nrpt },
+ ],
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "vpn" }], // Was tripped earlier this session
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "proxy" }], // Was tripped earlier this session
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "nrpt" }],
+ ].concat(falseExpectations(["vpn", "proxy", "nrpt"]))
+ );
+
+ mockedLinkService.platformDNSIndications =
+ Ci.nsINetworkLinkService.NONE_DETECTED;
+ simulateNetworkChange();
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "netchange");
+ checkScalars(
+ [
+ ["networking.doh_heuristics_attempts", { value: 5 }],
+ ["networking.doh_heuristics_pass_count", { value: 2 }],
+ [
+ "networking.doh_heuristics_result",
+ { value: Heuristics.Telemetry.pass },
+ ],
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "vpn" }], // Was tripped earlier this session
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "proxy" }], // Was tripped earlier this session
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "nrpt" }], // Was tripped earlier this session
+ ].concat(falseExpectations(["vpn", "proxy", "nrpt"]))
+ );
+});
diff --git a/browser/components/doh/test/browser/browser_policyOverride.js b/browser/components/doh/test/browser/browser_policyOverride.js
new file mode 100644
index 0000000000..02d851d947
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_policyOverride.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+add_task(async function testPolicyOverride() {
+ // Set up an arbitrary enterprise policy. Its existence should be sufficient
+ // to disable heuristics.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ EnableTrackingProtection: {
+ Value: true,
+ },
+ },
+ });
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.ACTIVE,
+ "Policy engine is active."
+ );
+
+ Preferences.set(prefs.ENABLED_PREF, true);
+ await waitForStateTelemetry(["shutdown", "policyDisabled"]);
+ is(
+ Preferences.get(prefs.BREADCRUMB_PREF),
+ undefined,
+ "Breadcrumb not saved."
+ );
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ undefined,
+ "TRR selection not performed."
+ );
+ is(
+ Preferences.get(prefs.SKIP_HEURISTICS_PREF),
+ true,
+ "Pref set to suppress CFR."
+ );
+ ensureNoTRRSelectionTelemetry();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+
+ checkScalars(
+ [
+ [
+ "networking.doh_heuristics_result",
+ { value: Heuristics.Telemetry.enterprisePresent },
+ ],
+ // All of the heuristics must be false.
+ ].concat(falseExpectations([]))
+ );
+
+ // Simulate a network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+
+ // Clean up.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {},
+ });
+ EnterprisePolicyTesting.resetRunOnceState();
+
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.INACTIVE,
+ "Policy engine is inactive at the end of the test"
+ );
+});
diff --git a/browser/components/doh/test/browser/browser_providerSteering.js b/browser/components/doh/test/browser/browser_providerSteering.js
new file mode 100644
index 0000000000..dc8b2cdf26
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_providerSteering.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const TEST_DOMAIN = "doh.test.";
+const AUTO_TRR_URI = "https://example.com/dns-query";
+
+add_task(setup);
+
+add_task(async function testProviderSteering() {
+ setPassingHeuristics();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
+ Preferences.set(prefs.ENABLED_PREF, true);
+ await prefPromise;
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ let providerTestcases = [
+ {
+ id: "provider1",
+ canonicalName: "foo.provider1.com",
+ uri: "https://foo.provider1.com/query",
+ },
+ {
+ id: "provider2",
+ canonicalName: "bar.provider2.com",
+ uri: "https://bar.provider2.com/query",
+ },
+ ];
+ let configFlushPromise = DoHTestUtils.waitForConfigFlush();
+ Preferences.set(
+ prefs.PROVIDER_STEERING_LIST_PREF,
+ JSON.stringify(providerTestcases)
+ );
+ await configFlushPromise;
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ let testNetChangeResult = async (
+ expectedURI,
+ heuristicsDecision,
+ providerName
+ ) => {
+ let trrURIChanged = TestUtils.topicObserved(
+ "network:trr-uri-changed",
+ () => {
+ // We need this check because this topic is observed once immediately
+ // after the network change when the URI is reset, and then when the
+ // provider steering heuristic runs and sets it to our uri.
+ return Services.dns.currentTrrURI == expectedURI;
+ }
+ );
+ simulateNetworkChange();
+ await trrURIChanged;
+ is(
+ Services.dns.currentTrrURI,
+ expectedURI,
+ `TRR URI set to ${expectedURI}`
+ );
+ await checkHeuristicsTelemetry(
+ heuristicsDecision,
+ "netchange",
+ providerName
+ );
+ };
+
+ for (let { id, canonicalName, uri } of providerTestcases) {
+ gDNSOverride.addIPOverride(TEST_DOMAIN, "9.9.9.9");
+ gDNSOverride.setCnameOverride(TEST_DOMAIN, canonicalName);
+ await testNetChangeResult(uri, "enable_doh", id);
+ gDNSOverride.clearHostOverride(TEST_DOMAIN);
+ }
+
+ await testNetChangeResult(AUTO_TRR_URI, "enable_doh");
+
+ // Just use the first provider for the remaining checks.
+ let provider = providerTestcases[0];
+ gDNSOverride.addIPOverride(TEST_DOMAIN, "9.9.9.9");
+ gDNSOverride.setCnameOverride(TEST_DOMAIN, provider.canonicalName);
+ await testNetChangeResult(provider.uri, "enable_doh", provider.id);
+
+ // Trigger safesearch heuristics and ensure provider steering is disabled.
+ let googleDomain = "google.com.";
+ let googleIP = "1.1.1.1";
+ let googleSafeSearchIP = "1.1.1.2";
+ gDNSOverride.clearHostOverride(googleDomain);
+ gDNSOverride.addIPOverride(googleDomain, googleSafeSearchIP);
+ await testNetChangeResult(AUTO_TRR_URI, "disable_doh");
+ gDNSOverride.clearHostOverride(googleDomain);
+ gDNSOverride.addIPOverride(googleDomain, googleIP);
+ checkScalars(
+ [
+ [
+ "networking.doh_heuristics_result",
+ { value: Heuristics.Telemetry.google },
+ ],
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "google" }],
+ // All of the other heuristics must be false.
+ ].concat(falseExpectations(["google"]))
+ );
+
+ // Check that provider steering is enabled again after we reset above.
+ await testNetChangeResult(provider.uri, "enable_doh", provider.id);
+
+ // Finally, provider steering should be disabled once we clear the override.
+ gDNSOverride.clearHostOverride(TEST_DOMAIN);
+ await testNetChangeResult(AUTO_TRR_URI, "enable_doh");
+
+ checkScalars(
+ [
+ [
+ "networking.doh_heuristics_result",
+ { value: Heuristics.Telemetry.pass },
+ ],
+ ["networking.doh_heuristic_ever_tripped", { value: true, key: "google" }],
+ // All of the other heuristics must be false.
+ ].concat(falseExpectations(["google"]))
+ );
+});
diff --git a/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js b/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js
new file mode 100644
index 0000000000..cd4356ed3f
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+add_task(setupRegion);
+
+async function setPrefAndWaitForConfigFlush(pref, value) {
+ let configFlushedPromise = DoHTestUtils.waitForConfigFlush();
+ Preferences.set(pref, value);
+ await configFlushedPromise;
+}
+
+async function clearPrefAndWaitForConfigFlush(pref, value) {
+ let configFlushedPromise = DoHTestUtils.waitForConfigFlush();
+ Preferences.reset(pref);
+ await configFlushedPromise;
+}
+
+add_task(async function testNewProfile() {
+ is(
+ DoHConfigController.currentConfig.enabled,
+ false,
+ "Rollout should not be enabled"
+ );
+
+ let provider1 = {
+ id: "provider1",
+ uri: "https://example.org/1",
+ autoDefault: true,
+ };
+ let provider2 = {
+ id: "provider2",
+ uri: "https://example.org/2",
+ canonicalName: "https://example.org/cname",
+ };
+ let provider3 = {
+ id: "provider3",
+ uri: "https://example.org/3",
+ autoDefault: true,
+ };
+
+ await DoHTestUtils.loadRemoteSettingsProviders([
+ provider1,
+ provider2,
+ provider3,
+ ]);
+
+ await DoHTestUtils.loadRemoteSettingsConfig({
+ id: kTestRegion.toLowerCase(),
+ rolloutEnabled: true,
+ providers: "provider1, provider3",
+ steeringEnabled: true,
+ steeringProviders: "provider2",
+ autoDefaultEnabled: true,
+ autoDefaultProviders: "provider1, provider3",
+ });
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ true,
+ "Rollout should be enabled"
+ );
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+ Assert.deepEqual(
+ DoHConfigController.currentConfig.providerList,
+ [provider1, provider3],
+ "Provider list should be loaded"
+ );
+ is(
+ DoHConfigController.currentConfig.providerSteering.enabled,
+ true,
+ "Steering should be enabled"
+ );
+ Assert.deepEqual(
+ DoHConfigController.currentConfig.providerSteering.providerList,
+ [provider2],
+ "Steering provider list should be loaded"
+ );
+ is(
+ DoHConfigController.currentConfig.trrSelection.enabled,
+ true,
+ "TRR Selection should be enabled"
+ );
+ Assert.deepEqual(
+ DoHConfigController.currentConfig.trrSelection.providerList,
+ [provider1, provider3],
+ "TRR Selection provider list should be loaded"
+ );
+ is(
+ DoHConfigController.currentConfig.fallbackProviderURI,
+ provider1.uri,
+ "Fallback provider URI should be that of the first one"
+ );
+
+ // Test that overriding with prefs works.
+ await setPrefAndWaitForConfigFlush(prefs.PROVIDER_STEERING_PREF, false);
+ is(
+ DoHConfigController.currentConfig.providerSteering.enabled,
+ false,
+ "Provider steering should be disabled"
+ );
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ await setPrefAndWaitForConfigFlush(prefs.TRR_SELECT_ENABLED_PREF, false);
+ is(
+ DoHConfigController.currentConfig.trrSelection.enabled,
+ false,
+ "TRR selection should be disabled"
+ );
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ // Try a regional pref this time
+ await setPrefAndWaitForConfigFlush(
+ `${kRegionalPrefNamespace}.enabled`,
+ false
+ );
+ is(
+ DoHConfigController.currentConfig.enabled,
+ false,
+ "Rollout should be disabled"
+ );
+ await ensureTRRMode(undefined);
+ await ensureNoHeuristicsTelemetry();
+
+ await clearPrefAndWaitForConfigFlush(`${kRegionalPrefNamespace}.enabled`);
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ true,
+ "Rollout should be enabled"
+ );
+
+ await DoHTestUtils.resetRemoteSettingsConfig();
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ false,
+ "Rollout should be disabled"
+ );
+ await ensureTRRMode(undefined);
+});
diff --git a/browser/components/doh/test/browser/browser_remoteSettings_rollout.js b/browser/components/doh/test/browser/browser_remoteSettings_rollout.js
new file mode 100644
index 0000000000..e0e31dd238
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_remoteSettings_rollout.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+add_task(setupRegion);
+
+add_task(async function testPrefFirstRollout() {
+ let defaults = Services.prefs.getDefaultBranch("");
+
+ setPassingHeuristics();
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ false,
+ "Rollout should not be enabled"
+ );
+
+ let configFlushedPromise = DoHTestUtils.waitForConfigFlush();
+ defaults.setBoolPref(`${kRegionalPrefNamespace}.enabled`, true);
+ await configFlushedPromise;
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ true,
+ "Rollout should be enabled"
+ );
+ await ensureTRRMode(2);
+
+ await DoHTestUtils.loadRemoteSettingsProviders([
+ {
+ id: "provider1",
+ uri: "https://example.org/1",
+ autoDefault: true,
+ },
+ ]);
+
+ await DoHTestUtils.loadRemoteSettingsConfig({
+ id: kTestRegion.toLowerCase(),
+ rolloutEnabled: true,
+ providers: "provider1",
+ });
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ true,
+ "Rollout should still be enabled"
+ );
+
+ defaults.deleteBranch(`${kRegionalPrefNamespace}.enabled`);
+ await restartDoHController();
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ true,
+ "Rollout should still be enabled"
+ );
+ await ensureTRRMode(2);
+
+ await DoHTestUtils.resetRemoteSettingsConfig();
+
+ is(
+ DoHConfigController.currentConfig.enabled,
+ false,
+ "Rollout should not be enabled"
+ );
+ await ensureTRRMode(undefined);
+});
diff --git a/browser/components/doh/test/browser/browser_rollback.js b/browser/components/doh/test/browser/browser_rollback.js
new file mode 100644
index 0000000000..b414c35ae5
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_rollback.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(setup);
+
+add_task(async function testRollback() {
+ // Set up a passing environment and enable DoH.
+ setPassingHeuristics();
+ let promise = waitForDoorhanger();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
+ Preferences.set(prefs.ENABLED_PREF, true);
+
+ await prefPromise;
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/dns-query",
+ "TRR selection complete."
+ );
+ await checkTRRSelectionTelemetry();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, EXAMPLE_URL);
+ let panel = await promise;
+
+ prefPromise = TestUtils.waitForPrefChange(
+ prefs.DOORHANGER_USER_DECISION_PREF
+ );
+
+ // Click the doorhanger's "accept" button.
+ let button = panel.querySelector(".popup-notification-primary-button");
+ promise = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await promise;
+
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ await prefPromise;
+ is(
+ Preferences.get(prefs.DOORHANGER_USER_DECISION_PREF),
+ "UIOk",
+ "Doorhanger decision saved."
+ );
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb not cleared.");
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Change the environment to failing and simulate a network change.
+ setFailingHeuristics();
+ simulateNetworkChange();
+ await ensureTRRMode(0);
+ await checkHeuristicsTelemetry("disable_doh", "netchange");
+
+ // Trigger another network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(0);
+ await checkHeuristicsTelemetry("disable_doh", "netchange");
+
+ // Rollback!
+ setPassingHeuristics();
+ Preferences.reset(prefs.ENABLED_PREF);
+ await waitForStateTelemetry(["shutdown", "rollback"]);
+ await ensureTRRMode(undefined);
+ ensureNoTRRSelectionTelemetry();
+ await ensureNoHeuristicsTelemetry();
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ await ensureNoHeuristicsTelemetry();
+
+ // Re-enable.
+ Preferences.set(prefs.ENABLED_PREF, true);
+
+ await ensureTRRMode(2);
+ ensureNoTRRSelectionTelemetry();
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ // Change the environment to failing and simulate a network change.
+ setFailingHeuristics();
+ simulateNetworkChange();
+ await ensureTRRMode(0);
+ await checkHeuristicsTelemetry("disable_doh", "netchange");
+
+ // Rollback again for good measure! This time with failing heuristics.
+ Preferences.reset(prefs.ENABLED_PREF);
+ await waitForStateTelemetry(["shutdown", "rollback"]);
+ await ensureTRRMode(undefined);
+ ensureNoTRRSelectionTelemetry();
+ await ensureNoHeuristicsTelemetry();
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ await ensureNoHeuristicsTelemetry();
+
+ // Re-enable.
+ Preferences.set(prefs.ENABLED_PREF, true);
+
+ await ensureTRRMode(0);
+ ensureNoTRRSelectionTelemetry();
+ await checkHeuristicsTelemetry("disable_doh", "startup");
+
+ // Change the environment to passing and simulate a network change.
+ setPassingHeuristics();
+ simulateNetworkChange();
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "netchange");
+
+ // Rollback again, this time with TRR mode set to 2 prior to doing so.
+ Preferences.reset(prefs.ENABLED_PREF);
+ await waitForStateTelemetry(["shutdown", "rollback"]);
+ await ensureTRRMode(undefined);
+ ensureNoTRRSelectionTelemetry();
+ await ensureNoHeuristicsTelemetry();
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ await ensureNoHeuristicsTelemetry();
+
+ // Re-enable.
+ Preferences.set(prefs.ENABLED_PREF, true);
+
+ await ensureTRRMode(2);
+ ensureNoTRRSelectionTelemetry();
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(2);
+ await checkHeuristicsTelemetry("enable_doh", "netchange");
+
+ // Rollback again. This time, uninit DoHController first to ensure it reacts
+ // correctly at startup.
+ await DoHController._uninit();
+ await waitForStateTelemetry(["shutdown"]);
+ Preferences.reset(prefs.ENABLED_PREF);
+ await DoHController.init();
+ await ensureTRRMode(undefined);
+ ensureNoTRRSelectionTelemetry();
+ await ensureNoHeuristicsTelemetry();
+ await waitForStateTelemetry(["rollback"]);
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ await ensureNoHeuristicsTelemetry();
+});
diff --git a/browser/components/doh/test/browser/browser_throttle_heuristics.js b/browser/components/doh/test/browser/browser_throttle_heuristics.js
new file mode 100644
index 0000000000..7a0b22ed11
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_throttle_heuristics.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+add_task(async function testHeuristicsThrottling() {
+ // Use a zero throttle timeout for the test. This both ensures the test has a
+ // short runtime as well as preventing intermittents because we thought
+ // something was deterministic when it wasn't.
+ let throttleTimeout = 0;
+ let rateLimit = 1;
+ let throttleDoneTopic = "doh:heuristics-throttle-done";
+ let throttleExtendTopic = "doh:heuristics-throttle-extend";
+
+ Preferences.set(prefs.HEURISTICS_THROTTLE_TIMEOUT_PREF, throttleTimeout);
+ Preferences.set(prefs.HEURISTICS_THROTTLE_RATE_LIMIT_PREF, rateLimit);
+
+ // Set up a passing environment and enable DoH.
+ let throttledPromise = TestUtils.topicObserved(throttleDoneTopic);
+ setPassingHeuristics();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
+ Preferences.set(prefs.ENABLED_PREF, true);
+
+ await prefPromise;
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ await ensureTRRMode(2);
+ info("waiting for throttle done");
+ await throttledPromise;
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ // Change the environment to failing and simulate a network change.
+ throttledPromise = TestUtils.topicObserved(throttleDoneTopic);
+ simulateNetworkChange();
+ info("waiting for throttle done");
+ await throttledPromise;
+ await checkHeuristicsTelemetry("enable_doh", "netchange");
+
+ /* Simulate two consecutive network changes and check that we throttled the
+ * second heuristics run. */
+
+ // We wait for the throttle timer to fire twice - the first time, a fresh
+ // heuristics run will be performed since it was queued while throttling.
+ // This triggers another throttle timeout which is the second one we wait for.
+ throttledPromise = TestUtils.topicObserved(throttleDoneTopic).then(() =>
+ TestUtils.topicObserved(throttleDoneTopic)
+ );
+ simulateNetworkChange();
+ simulateNetworkChange();
+ info("waiting for throttle done");
+ await throttledPromise;
+ await checkHeuristicsTelemetryMultiple(["netchange", "throttled"]);
+
+ /* Simulate several consecutive network changes at a rate that exceeds the
+ * rate limit and check that we only record two heuristics runs in total -
+ * one for the initial netchange and one throttled run at the end. */
+
+ // We wait for the throttle timer to be extended twice.
+ let throttleExtendPromise = TestUtils.topicObserved(throttleExtendTopic);
+ let throttleExtendPromise2 = throttleExtendPromise.then(() =>
+ TestUtils.topicObserved(throttleExtendTopic)
+ );
+
+ // Again, we wait for the timer to fire twice - once for the volatile period
+ // which results in a throttled heuristics run, and once after it with no run.
+ throttledPromise = throttleExtendPromise2
+ .then(() => TestUtils.topicObserved(throttleDoneTopic))
+ .then(() => TestUtils.topicObserved(throttleDoneTopic));
+
+ // Simulate three network changes:
+ // - The first one starts the throttle timer
+ // - The second one is within the limit of 1.
+ // - The third one exceeds the limit and extends the throttle period.
+ simulateNetworkChange();
+ simulateNetworkChange();
+ simulateNetworkChange();
+
+ // First throttle extension should happen now.
+ info("waiting for throttle extend");
+ await throttleExtendPromise;
+
+ // Two more network changes to once again extend the throttle period.
+ simulateNetworkChange();
+ simulateNetworkChange();
+
+ // Now the second extension should be detected.
+ info("waiting for throttle done");
+ await throttleExtendPromise2;
+
+ // Finally, we wait for the throttle period to finish.
+ info("waiting for throttle done");
+ await throttledPromise;
+
+ await checkHeuristicsTelemetryMultiple(["netchange", "throttled"]);
+});
diff --git a/browser/components/doh/test/browser/browser_trrSelect.js b/browser/components/doh/test/browser/browser_trrSelect.js
new file mode 100644
index 0000000000..68861be8b8
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_trrSelect.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+async function waitForStartup() {
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+}
+
+async function setPrefAndWaitForConfigFlush(pref, value) {
+ let configFlushed = DoHTestUtils.waitForConfigFlush();
+ if (value) {
+ Preferences.set(pref, value);
+ } else {
+ Preferences.reset(pref);
+ }
+ await configFlushed;
+ await waitForStartup();
+}
+
+add_task(setup);
+
+add_task(async function testTRRSelect() {
+ // Clean start: doh-rollout.uri should be set after init.
+ setPassingHeuristics();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
+ Preferences.set(prefs.ENABLED_PREF, true);
+ await prefPromise;
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/dns-query",
+ "TRR selection complete."
+ );
+
+ // Wait for heuristics to complete.
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ // Reset and restart the controller for good measure.
+ Preferences.reset(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF);
+ Preferences.reset(prefs.TRR_SELECT_URI_PREF);
+ await restartDoHController();
+ await waitForStartup();
+
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/dns-query",
+ "TRR selection complete."
+ );
+
+ // Disable committing. The committed URI should be reset to the
+ // default provider and the dry-run-result should persist.
+ prefPromise = TestUtils.waitForPrefChange(
+ prefs.TRR_SELECT_URI_PREF,
+ newVal => newVal == "https://example.com/1"
+ );
+ await setPrefAndWaitForConfigFlush(prefs.TRR_SELECT_COMMIT_PREF, false);
+ await prefPromise;
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/1",
+ "Default TRR selected."
+ );
+ try {
+ await BrowserTestUtils.waitForCondition(() => {
+ return !Preferences.isSet(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF);
+ });
+ ok(false, "Dry run result was cleared, fail!");
+ } catch (e) {
+ ok(true, "Dry run result was not cleared.");
+ }
+ is(
+ Preferences.get(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF),
+ "https://example.com/dns-query",
+ "dry-run result has the correct value."
+ );
+
+ // Reset again, dry-run-result should be recorded but not
+ // be committed. Committing is still disabled from above.
+ Preferences.reset(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF);
+ Preferences.reset(prefs.TRR_SELECT_URI_PREF);
+ await restartDoHController();
+ await waitForStartup();
+
+ try {
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ Preferences.get(prefs.TRR_SELECT_URI_PREF) ==
+ "https://example.com/dns-query"
+ );
+ });
+ ok(false, "Dry run result got committed, fail!");
+ } catch (e) {
+ ok(true, "Dry run result did not get committed");
+ }
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/1",
+ "Default TRR selected."
+ );
+ is(
+ Preferences.get(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF),
+ "https://example.com/dns-query",
+ "TRR selection complete, dry-run result recorded."
+ );
+
+ // Reset doh-rollout.uri, and change the dry-run-result to another one on the
+ // default list. After init, the existing dry-run-result should be committed.
+ Preferences.reset(prefs.TRR_SELECT_URI_PREF);
+ Preferences.set(
+ prefs.TRR_SELECT_DRY_RUN_RESULT_PREF,
+ "https://example.com/2"
+ );
+ prefPromise = TestUtils.waitForPrefChange(
+ prefs.TRR_SELECT_URI_PREF,
+ newVal => newVal == "https://example.com/2"
+ );
+ await setPrefAndWaitForConfigFlush(prefs.TRR_SELECT_COMMIT_PREF, true);
+ await prefPromise;
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/2",
+ "TRR selection complete, existing dry-run-result committed."
+ );
+
+ // Reset doh-rollout.uri, and change the dry-run-result to another one NOT on
+ // default list. After init, a new TRR should be selected and committed.
+ prefPromise = TestUtils.waitForPrefChange(
+ prefs.TRR_SELECT_URI_PREF,
+ newVal => newVal == "https://example.com/dns-query"
+ );
+ Preferences.reset(prefs.TRR_SELECT_URI_PREF);
+ Preferences.set(
+ prefs.TRR_SELECT_DRY_RUN_RESULT_PREF,
+ "https://example.com/4"
+ );
+ await restartDoHController();
+ await prefPromise;
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/dns-query",
+ "TRR selection complete, existing dry-run-result discarded and refreshed."
+ );
+});
diff --git a/browser/components/doh/test/browser/browser_trrSelection_disable.js b/browser/components/doh/test/browser/browser_trrSelection_disable.js
new file mode 100644
index 0000000000..dc7bd68262
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_trrSelection_disable.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+add_task(async function testTrrSelectionDisable() {
+ // Turn off TRR Selection.
+ let configFlushed = DoHTestUtils.waitForConfigFlush();
+ Preferences.set(prefs.TRR_SELECT_ENABLED_PREF, false);
+ await configFlushed;
+
+ // Set up a passing environment and enable DoH.
+ setPassingHeuristics();
+ let promise = waitForDoorhanger();
+ Preferences.set(prefs.ENABLED_PREF, true);
+ await BrowserTestUtils.waitForCondition(() => {
+ return Preferences.get(prefs.BREADCRUMB_PREF);
+ });
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ is(
+ Preferences.get(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF),
+ undefined,
+ "TRR selection dry run not performed."
+ );
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/1",
+ "doh-rollout.uri set to first provider in the list."
+ );
+ ensureNoTRRSelectionTelemetry();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, EXAMPLE_URL);
+ let panel = await promise;
+
+ // Click the doorhanger's "accept" button.
+ let button = panel.querySelector(".popup-notification-primary-button");
+ promise = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await promise;
+
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return Preferences.get(prefs.DOORHANGER_USER_DECISION_PREF);
+ });
+ is(
+ Preferences.get(prefs.DOORHANGER_USER_DECISION_PREF),
+ "UIOk",
+ "Doorhanger decision saved."
+ );
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb not cleared.");
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Restart the controller for good measure.
+ await restartDoHController();
+ ensureNoTRRSelectionTelemetry();
+ is(
+ Preferences.get(prefs.TRR_SELECT_DRY_RUN_RESULT_PREF),
+ undefined,
+ "TRR selection dry run not performed."
+ );
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/1",
+ "doh-rollout.uri set to first provider in the list."
+ );
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+});
diff --git a/browser/components/doh/test/browser/browser_userInterference.js b/browser/components/doh/test/browser/browser_userInterference.js
new file mode 100644
index 0000000000..96d1e3c94c
--- /dev/null
+++ b/browser/components/doh/test/browser/browser_userInterference.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+add_task(async function testUserInterference() {
+ // Set up a passing environment and enable DoH.
+ setPassingHeuristics();
+ let promise = waitForDoorhanger();
+ let prefPromise = TestUtils.waitForPrefChange(prefs.BREADCRUMB_PREF);
+ Preferences.set(prefs.ENABLED_PREF, true);
+
+ await prefPromise;
+ is(Preferences.get(prefs.BREADCRUMB_PREF), true, "Breadcrumb saved.");
+ is(
+ Preferences.get(prefs.TRR_SELECT_URI_PREF),
+ "https://example.com/dns-query",
+ "TRR selection complete."
+ );
+ await checkTRRSelectionTelemetry();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, EXAMPLE_URL);
+ let panel = await promise;
+
+ prefPromise = TestUtils.waitForPrefChange(
+ prefs.DOORHANGER_USER_DECISION_PREF
+ );
+
+ // Click the doorhanger's "accept" button.
+ let button = panel.querySelector(".popup-notification-primary-button");
+ promise = BrowserTestUtils.waitForEvent(panel, "popuphidden");
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await promise;
+ await prefPromise;
+
+ is(
+ Preferences.get(prefs.DOORHANGER_USER_DECISION_PREF),
+ "UIOk",
+ "Doorhanger decision saved."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ await ensureTRRMode(2);
+ await checkHeuristicsTelemetry("enable_doh", "startup");
+
+ // Set the TRR mode pref manually and ensure we respect this.
+ Preferences.set(prefs.NETWORK_TRR_MODE_PREF, 3);
+ await ensureTRRMode(undefined);
+
+ // Simulate a network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+
+ is(
+ Preferences.get(prefs.DISABLED_PREF, false),
+ true,
+ "Manual disable recorded."
+ );
+ is(Preferences.get(prefs.BREADCRUMB_PREF), undefined, "Breadcrumb cleared.");
+
+ // Simulate another network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+
+ // Restart the controller for good measure.
+ await restartDoHController();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoTRRSelectionTelemetry();
+ ensureNoHeuristicsTelemetry();
+
+ // Simulate another network change.
+ simulateNetworkChange();
+ await ensureNoTRRModeChange(undefined);
+ ensureNoHeuristicsTelemetry();
+});
diff --git a/browser/components/doh/test/browser/head.js b/browser/components/doh/test/browser/head.js
new file mode 100644
index 0000000000..c5c4c1c16b
--- /dev/null
+++ b/browser/components/doh/test/browser/head.js
@@ -0,0 +1,408 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
+ DoHConfigController: "resource:///modules/DoHConfig.sys.mjs",
+ DoHController: "resource:///modules/DoHController.sys.mjs",
+ DoHTestUtils: "resource://testing-common/DoHTestUtils.sys.mjs",
+ Heuristics: "resource:///modules/DoHHeuristics.sys.mjs",
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+ Region: "resource://gre/modules/Region.sys.mjs",
+ RegionTestUtils: "resource://testing-common/RegionTestUtils.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gDNSOverride",
+ "@mozilla.org/network/native-dns-override;1",
+ "nsINativeDNSResolverOverride"
+);
+
+const { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+
+const EXAMPLE_URL = "https://example.com/";
+
+const prefs = {
+ TESTING_PREF: "doh-rollout._testing",
+ ENABLED_PREF: "doh-rollout.enabled",
+ ROLLOUT_TRR_MODE_PREF: "doh-rollout.mode",
+ NETWORK_TRR_MODE_PREF: "network.trr.mode",
+ CONFIRMATION_NS_PREF: "network.trr.confirmationNS",
+ BREADCRUMB_PREF: "doh-rollout.self-enabled",
+ DOORHANGER_USER_DECISION_PREF: "doh-rollout.doorhanger-decision",
+ DISABLED_PREF: "doh-rollout.disable-heuristics",
+ SKIP_HEURISTICS_PREF: "doh-rollout.skipHeuristicsCheck",
+ CLEAR_ON_SHUTDOWN_PREF: "doh-rollout.clearModeOnShutdown",
+ FIRST_RUN_PREF: "doh-rollout.doneFirstRun",
+ PROVIDER_LIST_PREF: "doh-rollout.provider-list",
+ TRR_SELECT_ENABLED_PREF: "doh-rollout.trr-selection.enabled",
+ TRR_SELECT_URI_PREF: "doh-rollout.uri",
+ TRR_SELECT_COMMIT_PREF: "doh-rollout.trr-selection.commit-result",
+ TRR_SELECT_DRY_RUN_RESULT_PREF: "doh-rollout.trr-selection.dry-run-result",
+ PROVIDER_STEERING_PREF: "doh-rollout.provider-steering.enabled",
+ PROVIDER_STEERING_LIST_PREF: "doh-rollout.provider-steering.provider-list",
+ NETWORK_DEBOUNCE_TIMEOUT_PREF: "doh-rollout.network-debounce-timeout",
+ HEURISTICS_THROTTLE_TIMEOUT_PREF: "doh-rollout.heuristics-throttle-timeout",
+ HEURISTICS_THROTTLE_RATE_LIMIT_PREF:
+ "doh-rollout.heuristics-throttle-rate-limit",
+};
+
+const CFR_PREF = "browser.newtabpage.activity-stream.asrouter.providers.cfr";
+const CFR_JSON = {
+ id: "cfr",
+ enabled: true,
+ type: "local",
+ localProvider: "CFRMessageProvider",
+ categories: ["cfrAddons", "cfrFeatures"],
+};
+
+async function setup() {
+ await DoHController._uninit();
+ await DoHConfigController._uninit();
+ SpecialPowers.pushPrefEnv({
+ set: [["security.notification_enable_delay", 0]],
+ });
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.telemetry.clearEvents();
+ Services.telemetry.clearScalars();
+
+ // Enable the CFR.
+ Preferences.set(CFR_PREF, JSON.stringify(CFR_JSON));
+
+ // Tell DoHController that this isn't real life.
+ Preferences.set(prefs.TESTING_PREF, true);
+
+ // Avoid non-local connections to the TRR endpoint.
+ Preferences.set(prefs.CONFIRMATION_NS_PREF, "skip");
+
+ // Enable trr selection and provider steeringfor tests. This is off
+ // by default so it can be controlled via Normandy.
+ Preferences.set(prefs.TRR_SELECT_ENABLED_PREF, true);
+ Preferences.set(prefs.PROVIDER_STEERING_PREF, true);
+
+ // Enable committing the TRR selection. This pref ships false by default so
+ // it can be controlled e.g. via Normandy, but for testing let's set enable.
+ Preferences.set(prefs.TRR_SELECT_COMMIT_PREF, true);
+
+ // Clear mode on shutdown by default.
+ Preferences.set(prefs.CLEAR_ON_SHUTDOWN_PREF, true);
+
+ // Generally don't bother with debouncing or throttling.
+ // The throttling test will set this explicitly.
+ Preferences.set(prefs.NETWORK_DEBOUNCE_TIMEOUT_PREF, -1);
+ Preferences.set(prefs.HEURISTICS_THROTTLE_TIMEOUT_PREF, -1);
+
+ // Set up heuristics, all passing by default.
+
+ // Google safesearch overrides
+ gDNSOverride.addIPOverride("www.google.com.", "1.1.1.1");
+ gDNSOverride.addIPOverride("google.com.", "1.1.1.1");
+ gDNSOverride.addIPOverride("forcesafesearch.google.com.", "1.1.1.2");
+
+ // YouTube safesearch overrides
+ gDNSOverride.addIPOverride("www.youtube.com.", "2.1.1.1");
+ gDNSOverride.addIPOverride("m.youtube.com.", "2.1.1.1");
+ gDNSOverride.addIPOverride("youtubei.googleapis.com.", "2.1.1.1");
+ gDNSOverride.addIPOverride("youtube.googleapis.com.", "2.1.1.1");
+ gDNSOverride.addIPOverride("www.youtube-nocookie.com.", "2.1.1.1");
+ gDNSOverride.addIPOverride("restrict.youtube.com.", "2.1.1.2");
+ gDNSOverride.addIPOverride("restrictmoderate.youtube.com.", "2.1.1.2");
+
+ // Zscaler override
+ gDNSOverride.addIPOverride("sitereview.zscaler.com.", "3.1.1.1");
+
+ // Global canary
+ gDNSOverride.addIPOverride("use-application-dns.net.", "4.1.1.1");
+
+ await DoHTestUtils.resetRemoteSettingsConfig(false);
+
+ await DoHConfigController.init();
+ await DoHController.init();
+
+ await waitForStateTelemetry(["rollback"]);
+
+ registerCleanupFunction(async () => {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.telemetry.clearEvents();
+ gDNSOverride.clearOverrides();
+ if (ASRouter.state.messageBlockList.includes("DOH_ROLLOUT_CONFIRMATION")) {
+ await ASRouter.unblockMessageById("DOH_ROLLOUT_CONFIRMATION");
+ }
+ // The CFR pref is set to an empty array in user.js for testing profiles,
+ // so "reset" it back to that value.
+ Preferences.set(CFR_PREF, "[]");
+ await DoHController._uninit();
+ Services.telemetry.clearEvents();
+ Preferences.reset(Object.values(prefs));
+ await DoHTestUtils.resetRemoteSettingsConfig(false);
+ await DoHController.init();
+ });
+}
+
+const kTestRegion = "DE";
+const kRegionalPrefNamespace = `doh-rollout.${kTestRegion.toLowerCase()}`;
+
+async function setupRegion() {
+ Region._home = null;
+ RegionTestUtils.setNetworkRegion(kTestRegion);
+ await Region._fetchRegion();
+ is(Region.home, kTestRegion, "Should have correct region");
+ Preferences.reset("doh-rollout.home-region");
+ await DoHConfigController.loadRegion();
+}
+
+async function checkTRRSelectionTelemetry() {
+ let events;
+ await TestUtils.waitForCondition(() => {
+ events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ return events && events.length;
+ });
+ events = events.filter(
+ e =>
+ e[1] == "security.doh.trrPerformance" &&
+ e[2] == "trrselect" &&
+ e[3] == "dryrunresult"
+ );
+ is(events.length, 1, "Found the expected trrselect event.");
+ is(
+ events[0][4],
+ "https://example.com/dns-query",
+ "The event records the expected decision"
+ );
+}
+
+function ensureNoTRRSelectionTelemetry() {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ if (!events) {
+ ok(true, "Found no trrselect events.");
+ return;
+ }
+ events = events.filter(
+ e =>
+ e[1] == "security.doh.trrPerformance" &&
+ e[2] == "trrselect" &&
+ e[3] == "dryrunresult"
+ );
+ is(events.length, 0, "Found no trrselect events.");
+}
+
+async function checkHeuristicsTelemetry(
+ decision,
+ evaluateReason,
+ steeredProvider = ""
+) {
+ let events;
+ await TestUtils.waitForCondition(() => {
+ events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ events = events?.filter(
+ e => e[1] == "doh" && e[2] == "evaluate_v2" && e[3] == "heuristics"
+ );
+ return events?.length;
+ });
+ is(events.length, 1, "Found the expected heuristics event.");
+ is(events[0][4], decision, "The event records the expected decision");
+ if (evaluateReason) {
+ is(events[0][5].evaluateReason, evaluateReason, "Got the expected reason.");
+ }
+ is(events[0][5].steeredProvider, steeredProvider, "Got expected provider.");
+
+ // After checking the event, clear all telemetry. Since we check for a single
+ // event above, this ensures all heuristics events are intentional and tested.
+ // TODO: Test events other than heuristics. Those tests would also work the
+ // same way, so as to test one event at a time, and this clearEvents() call
+ // will continue to exist as-is.
+ Services.telemetry.clearEvents();
+}
+
+// Generates an array of expectations for the ever_tripped scalar
+// containing false and key, except for the keyes contained in
+// the `except` parameter.
+function falseExpectations(except) {
+ return Heuristics.Telemetry.heuristicNames()
+ .map(e => [
+ "networking.doh_heuristic_ever_tripped",
+ { value: false, key: e },
+ ])
+ .filter(e => except && !except.includes(e[1].key));
+}
+
+function checkScalars(expectations) {
+ // expectations: [[scalarname: expectationObject]]
+ // expectationObject: {value, key}
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ let keyedSnapshot = TelemetryTestUtils.getProcessScalars(
+ "parent",
+ true,
+ false
+ );
+ for (let ex of expectations) {
+ let scalarName = ex[0];
+ let exObject = ex[1];
+ if (exObject.key) {
+ TelemetryTestUtils.assertKeyedScalar(
+ keyedSnapshot,
+ scalarName,
+ exObject.key,
+ exObject.value,
+ `${scalarName} expected to have ${exObject.value}, key: ${exObject.key}`
+ );
+ } else {
+ TelemetryTestUtils.assertScalar(
+ snapshot,
+ scalarName,
+ exObject.value,
+ `${scalarName} expected to have ${exObject.value}`
+ );
+ }
+ }
+}
+
+async function checkHeuristicsTelemetryMultiple(expectedEvaluateReasons) {
+ let events;
+ await TestUtils.waitForCondition(() => {
+ events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ if (events && events.length) {
+ events = events.filter(
+ e => e[1] == "doh" && e[2] == "evaluate_v2" && e[3] == "heuristics"
+ );
+ if (events.length == expectedEvaluateReasons.length) {
+ return true;
+ }
+ }
+ return false;
+ });
+ is(
+ events.length,
+ expectedEvaluateReasons.length,
+ "Found the expected heuristics events."
+ );
+ for (let reason of expectedEvaluateReasons) {
+ let event = events.find(e => e[5].evaluateReason == reason);
+ is(event[5].evaluateReason, reason, `${reason} event found`);
+ }
+ Services.telemetry.clearEvents();
+}
+
+function ensureNoHeuristicsTelemetry() {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ if (!events) {
+ ok(true, "Found no heuristics events.");
+ return;
+ }
+ events = events.filter(
+ e => e[1] == "doh" && e[2] == "evaluate_v2" && e[3] == "heuristics"
+ );
+ is(events.length, 0, "Found no heuristics events.");
+}
+
+async function waitForStateTelemetry(expectedStates) {
+ let events;
+ await TestUtils.waitForCondition(() => {
+ events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ).parent;
+ return events;
+ });
+ events = events.filter(e => e[1] == "doh" && e[2] == "state");
+ info(events);
+ is(events.length, expectedStates.length, "Found the expected state events.");
+ for (let state of expectedStates) {
+ let event = events.find(e => e[3] == state);
+ is(event[3], state, `${state} state found`);
+ }
+ Services.telemetry.clearEvents();
+}
+
+async function restartDoHController() {
+ let oldMode = Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF);
+ await DoHController._uninit();
+ let newMode = Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF);
+ let expectClear = Preferences.get(prefs.CLEAR_ON_SHUTDOWN_PREF);
+ is(
+ newMode,
+ expectClear ? undefined : oldMode,
+ `Mode was ${expectClear ? "cleared" : "persisted"} on shutdown.`
+ );
+ await DoHController.init();
+}
+
+// setPassing/FailingHeuristics are used generically to test that DoH is enabled
+// or disabled correctly. We use the zscaler canary arbitrarily here, individual
+// heuristics are tested separately.
+function setPassingHeuristics() {
+ gDNSOverride.clearHostOverride("sitereview.zscaler.com.");
+ gDNSOverride.addIPOverride("sitereview.zscaler.com.", "3.1.1.1");
+}
+
+function setFailingHeuristics() {
+ gDNSOverride.clearHostOverride("sitereview.zscaler.com.");
+ gDNSOverride.addIPOverride("sitereview.zscaler.com.", "213.152.228.242");
+}
+
+async function waitForDoorhanger() {
+ const popupID = "contextual-feature-recommendation";
+ const bucketID = "DOH_ROLLOUT_CONFIRMATION";
+ let panel;
+ await BrowserTestUtils.waitForEvent(document, "popupshown", true, event => {
+ panel = event.originalTarget;
+ let popupNotification = event.originalTarget.firstChild;
+ return (
+ popupNotification &&
+ popupNotification.notification &&
+ popupNotification.notification.id == popupID &&
+ popupNotification.getAttribute("data-notification-bucket") == bucketID
+ );
+ });
+ return panel;
+}
+
+function simulateNetworkChange() {
+ // The networkStatus API does not actually propagate the link status we supply
+ // here, but rather sends the link status from the NetworkLinkService.
+ // This means there's no point sending a down and then an up - the extension
+ // will just receive "up" twice.
+ // TODO: Implement a mock NetworkLinkService and use it to also simulate
+ // network down events.
+ Services.obs.notifyObservers(null, "network:link-status-changed", "up");
+}
+
+async function ensureTRRMode(mode) {
+ await TestUtils.waitForCondition(() => {
+ return Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF) === mode;
+ });
+ is(Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF), mode, `TRR mode is ${mode}`);
+}
+
+async function ensureNoTRRModeChange(mode) {
+ try {
+ // Try and wait for the TRR pref to change... waitForCondition should throw
+ // after trying for a while.
+ await TestUtils.waitForCondition(() => {
+ return Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF) !== mode;
+ });
+ // If we reach this, the waitForCondition didn't throw. Fail!
+ ok(false, "TRR mode changed when it shouldn't have!");
+ } catch (e) {
+ // Assert for clarity.
+ is(
+ Preferences.get(prefs.ROLLOUT_TRR_MODE_PREF),
+ mode,
+ "No change in TRR mode"
+ );
+ }
+}
diff --git a/browser/components/doh/test/unit/head.js b/browser/components/doh/test/unit/head.js
new file mode 100644
index 0000000000..3c8010cba3
--- /dev/null
+++ b/browser/components/doh/test/unit/head.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+let h2Port, trrServer1, trrServer2, trrList;
+let DNSLookup, LookupAggregator, TRRRacer;
+
+function readFile(file) {
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ let data = NetUtil.readInputStreamToString(fstream, fstream.available());
+ fstream.close();
+ return data;
+}
+
+function addCertFromFile(certdb, filename, trustString) {
+ let certFile = do_get_file(filename, false);
+ let pem = readFile(certFile)
+ .replace(/-----BEGIN CERTIFICATE-----/, "")
+ .replace(/-----END CERTIFICATE-----/, "")
+ .replace(/[\r\n]/g, "");
+ certdb.addCertFromBase64(pem, trustString);
+}
+
+function ensureNoTelemetry() {
+ let events =
+ Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).parent || [];
+ events = events.filter(e => e[1] == "security.doh.trrPerformance");
+ Assert.ok(!events.length);
+}
+
+function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ // Set to allow the cert presented by our H2 server
+ do_get_profile();
+
+ Services.prefs.setBoolPref("network.http.http2.enabled", true);
+
+ // use the h2 server as DOH provider
+ trrServer1 = `https://foo.example.com:${h2Port}/doh?responseIP=1.1.1.1`;
+ trrServer2 = `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`;
+ trrList = [trrServer1, trrServer2];
+ // make all native resolve calls "secretly" resolve localhost instead
+ Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+ // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+ // so add that cert to the trust list as a signing cert. // the foo.example.com domain name.
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ Services.prefs.setIntPref("doh-rollout.trrRace.randomSubdomainCount", 2);
+
+ Services.prefs.setCharPref(
+ "doh-rollout.trrRace.popularDomains",
+ "foo.example.com., bar.example.com."
+ );
+
+ Services.prefs.setCharPref(
+ "doh-rollout.trrRace.canonicalDomain",
+ "firefox-dns-perf-test.net."
+ );
+
+ let TRRPerformance = ChromeUtils.importESModule(
+ "resource:///modules/TRRPerformance.sys.mjs"
+ );
+
+ DNSLookup = TRRPerformance.DNSLookup;
+ LookupAggregator = TRRPerformance.LookupAggregator;
+ TRRRacer = TRRPerformance.TRRRacer;
+
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.http.http2.enabled");
+ Services.prefs.clearUserPref("network.dns.native-is-localhost");
+
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+}
diff --git a/browser/components/doh/test/unit/test_DNSLookup.js b/browser/components/doh/test/unit/test_DNSLookup.js
new file mode 100644
index 0000000000..62ac3f3cd3
--- /dev/null
+++ b/browser/components/doh/test/unit/test_DNSLookup.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+add_task(async function test_SuccessfulRandomDNSLookup() {
+ let deferred = Promise.withResolvers();
+ let lookup = new DNSLookup(
+ null,
+ trrServer1,
+ (request, record, status, usedDomain, retryCount) => {
+ deferred.resolve({ request, record, status, usedDomain, retryCount });
+ }
+ );
+ lookup.doLookup();
+ let result = await deferred.promise;
+ Assert.ok(result.usedDomain.endsWith(".firefox-dns-perf-test.net."));
+ Assert.equal(result.status, Cr.NS_OK);
+ Assert.ok(result.record.QueryInterface(Ci.nsIDNSAddrRecord));
+ Assert.ok(result.record.IsTRR());
+ Assert.greater(result.record.trrFetchDuration, 0);
+ Assert.equal(result.retryCount, 1);
+});
+
+add_task(async function test_SuccessfulSpecifiedDNSLookup() {
+ let deferred = Promise.withResolvers();
+ let lookup = new DNSLookup(
+ "foo.example.com",
+ trrServer1,
+ (request, record, status, usedDomain, retryCount) => {
+ deferred.resolve({ request, record, status, usedDomain, retryCount });
+ }
+ );
+ lookup.doLookup();
+ let result = await deferred.promise;
+ Assert.equal(result.usedDomain, "foo.example.com");
+ Assert.equal(result.status, Cr.NS_OK);
+ Assert.ok(result.record.QueryInterface(Ci.nsIDNSAddrRecord));
+ Assert.ok(result.record.IsTRR());
+ Assert.greater(result.record.trrFetchDuration, 0);
+ Assert.equal(result.retryCount, 1);
+});
+
+add_task(async function test_FailedDNSLookup() {
+ let deferred = Promise.withResolvers();
+ let lookup = new DNSLookup(
+ null,
+ `https://foo.example.com:${h2Port}/doh?responseIP=none`,
+ (request, record, status, usedDomain, retryCount) => {
+ deferred.resolve({ request, record, status, usedDomain, retryCount });
+ }
+ );
+ lookup.doLookup();
+ let result = await deferred.promise;
+ Assert.ok(result.usedDomain.endsWith(".firefox-dns-perf-test.net."));
+ Assert.notEqual(result.status, Cr.NS_OK);
+ Assert.equal(result.record, null);
+ Assert.equal(result.retryCount, 3);
+});
diff --git a/browser/components/doh/test/unit/test_LookupAggregator.js b/browser/components/doh/test/unit/test_LookupAggregator.js
new file mode 100644
index 0000000000..e63920ee35
--- /dev/null
+++ b/browser/components/doh/test/unit/test_LookupAggregator.js
@@ -0,0 +1,162 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+add_task(setup);
+
+async function helper_SuccessfulLookupAggregator(
+ networkUnstable = false,
+ captivePortal = false
+) {
+ let deferred = Promise.withResolvers();
+ let aggregator = new LookupAggregator(() => deferred.resolve(), trrList);
+ // The aggregator's domain list should correctly reflect our set
+ // prefs for number of random subdomains (2) and the list of
+ // popular domains.
+ Assert.equal(aggregator.domains[0], null);
+ Assert.equal(aggregator.domains[1], null);
+ Assert.equal(aggregator.domains[2], "foo.example.com.");
+ Assert.equal(aggregator.domains[3], "bar.example.com.");
+ Assert.equal(aggregator.totalLookups, 8); // 2 TRRs * 4 domains.
+
+ if (networkUnstable) {
+ aggregator.markUnstableNetwork();
+ }
+ if (captivePortal) {
+ aggregator.markCaptivePortal();
+ }
+ aggregator.run();
+ await deferred.promise;
+ Assert.ok(!aggregator.aborted);
+ Assert.equal(aggregator.networkUnstable, networkUnstable);
+ Assert.equal(aggregator.captivePortal, captivePortal);
+ Assert.equal(aggregator.results.length, aggregator.totalLookups);
+
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).parent;
+ Assert.ok(events);
+ events = events.filter(e => e[1] == "security.doh.trrPerformance");
+ Assert.equal(events.length, aggregator.totalLookups);
+
+ for (let event of events) {
+ info(JSON.stringify(event));
+ Assert.equal(event[1], "security.doh.trrPerformance");
+ Assert.equal(event[2], "resolved");
+ Assert.equal(event[3], "record");
+ Assert.equal(event[4], "success");
+ }
+
+ // We only need to check the payload of each event from here on.
+ events = events.map(e => e[5]);
+
+ for (let trr of [trrServer1, trrServer2]) {
+ // There should be two results for random subdomains.
+ let results = aggregator.results.filter(r => {
+ return r.trr == trr && r.domain.endsWith(".firefox-dns-perf-test.net.");
+ });
+ Assert.equal(results.length, 2);
+
+ for (let result of results) {
+ Assert.ok(result.domain.endsWith(".firefox-dns-perf-test.net."));
+ Assert.equal(result.trr, trr);
+ Assert.ok(Components.isSuccessCode(result.status));
+ Assert.greater(result.time, 0);
+ Assert.equal(result.retryCount, 1);
+
+ let matchingEvents = events.filter(
+ e => e.domain == result.domain && e.trr == result.trr
+ );
+ Assert.equal(matchingEvents.length, 1);
+ let e = matchingEvents.pop();
+ for (let key of Object.keys(result)) {
+ Assert.equal(e[key], result[key].toString());
+ }
+ Assert.equal(e.networkUnstable, networkUnstable.toString());
+ Assert.equal(e.captivePortal, captivePortal.toString());
+ }
+
+ // There should be two results for the popular domains.
+ results = aggregator.results.filter(r => {
+ return r.trr == trr && !r.domain.endsWith(".firefox-dns-perf-test.net.");
+ });
+ Assert.equal(results.length, 2);
+
+ Assert.ok(
+ [results[0].domain, results[1].domain].includes("foo.example.com.")
+ );
+ Assert.ok(
+ [results[0].domain, results[1].domain].includes("bar.example.com.")
+ );
+ for (let result of results) {
+ Assert.equal(result.trr, trr);
+ Assert.equal(result.status, Cr.NS_OK);
+ Assert.greater(result.time, 0);
+ Assert.equal(result.retryCount, 1);
+
+ let matchingEvents = events.filter(
+ e => e.domain == result.domain && e.trr == result.trr
+ );
+ Assert.equal(matchingEvents.length, 1);
+ let e = matchingEvents.pop();
+ for (let key of Object.keys(result)) {
+ Assert.equal(e[key], result[key].toString());
+ }
+ Assert.equal(e.networkUnstable, networkUnstable.toString());
+ Assert.equal(e.captivePortal, captivePortal.toString());
+ }
+ }
+
+ Services.telemetry.clearEvents();
+}
+
+add_task(async function test_SuccessfulLookupAggregator() {
+ await helper_SuccessfulLookupAggregator(false, false);
+ await helper_SuccessfulLookupAggregator(false, true);
+ await helper_SuccessfulLookupAggregator(true, false);
+ await helper_SuccessfulLookupAggregator(true, true);
+});
+
+add_task(async function test_AbortedLookupAggregator() {
+ let deferred = Promise.withResolvers();
+ let aggregator = new LookupAggregator(() => deferred.resolve(), trrList);
+ // The aggregator's domain list should correctly reflect our set
+ // prefs for number of random subdomains (2) and the list of
+ // popular domains.
+ Assert.equal(aggregator.domains[0], null);
+ Assert.equal(aggregator.domains[1], null);
+ Assert.equal(aggregator.domains[2], "foo.example.com.");
+ Assert.equal(aggregator.domains[3], "bar.example.com.");
+ Assert.equal(aggregator.totalLookups, 8); // 2 TRRs * 4 domains.
+
+ // The aggregator should never call the onComplete callback. To test
+ // this, race the deferred promise with a 3 second timeout. The timeout
+ // should win, since the deferred promise should never resolve.
+ let timeoutPromise = new Promise(resolve => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(() => resolve("timeout"), 3000);
+ });
+ aggregator.run();
+ aggregator.abort();
+ let winner = await Promise.race([deferred.promise, timeoutPromise]);
+ Assert.equal(winner, "timeout");
+ Assert.ok(aggregator.aborted);
+ Assert.ok(!aggregator.networkUnstable);
+ Assert.ok(!aggregator.captivePortal);
+
+ // Ensure we send no telemetry for an aborted run!
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).parent;
+ Assert.ok(
+ !events || !events.filter(e => e[1] == "security.doh.trrPerformance").length
+ );
+});
diff --git a/browser/components/doh/test/unit/test_TRRRacer.js b/browser/components/doh/test/unit/test_TRRRacer.js
new file mode 100644
index 0000000000..9ad38f3981
--- /dev/null
+++ b/browser/components/doh/test/unit/test_TRRRacer.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_task(setup);
+
+add_task(async function test_TRRRacer_cleanRun() {
+ let deferred = Promise.withResolvers();
+ let racer = new TRRRacer(() => {
+ deferred.resolve();
+ deferred.resolved = true;
+ }, trrList);
+ racer.run();
+
+ await deferred.promise;
+ Assert.equal(racer._retryCount, 1);
+
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).parent;
+ Assert.ok(events);
+ events = events.filter(e => e[1] == "security.doh.trrPerformance");
+ Assert.equal(events.length, racer._aggregator.totalLookups);
+
+ Services.telemetry.clearEvents();
+
+ // Simulate network changes and ensure no re-runs since it's already complete.
+ async function testNetworkChange(captivePortal = false) {
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login");
+ } else {
+ Services.obs.notifyObservers(null, "network:link-status-changed", "down");
+ }
+
+ Assert.ok(!racer._aggregator.aborted);
+
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ } else {
+ Services.obs.notifyObservers(null, "network:link-status-changed", "up");
+ }
+
+ Assert.equal(racer._retryCount, 1);
+ ensureNoTelemetry();
+
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login-abort");
+ }
+ }
+
+ testNetworkChange(false);
+ testNetworkChange(true);
+});
+
+async function test_TRRRacer_networkFlux_helper(captivePortal = false) {
+ let deferred = Promise.withResolvers();
+ let racer = new TRRRacer(() => {
+ deferred.resolve();
+ deferred.resolved = true;
+ }, trrList);
+ racer.run();
+
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login");
+ } else {
+ Services.obs.notifyObservers(null, "network:link-status-changed", "down");
+ }
+
+ Assert.ok(racer._aggregator.aborted);
+ ensureNoTelemetry();
+ Assert.equal(racer._retryCount, 1);
+ Assert.ok(!deferred.resolved);
+
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ } else {
+ Services.obs.notifyObservers(null, "network:link-status-changed", "up");
+ }
+
+ Assert.ok(!racer._aggregator.aborted);
+ await deferred.promise;
+
+ Assert.equal(racer._retryCount, 2);
+
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).parent;
+ Assert.ok(events);
+ events = events.filter(e => e[1] == "security.doh.trrPerformance");
+ Assert.equal(events.length, racer._aggregator.totalLookups);
+
+ Services.telemetry.clearEvents();
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login-abort");
+ }
+}
+
+add_task(async function test_TRRRacer_networkFlux() {
+ await test_TRRRacer_networkFlux_helper(false);
+ await test_TRRRacer_networkFlux_helper(true);
+});
+
+async function test_TRRRacer_maxRetries_helper(captivePortal = false) {
+ let deferred = Promise.withResolvers();
+ let racer = new TRRRacer(() => {
+ deferred.resolve();
+ deferred.resolved = true;
+ }, trrList);
+ racer.run();
+ info("ran new racer");
+ // Start at i = 1 since we're already at retry #1.
+ for (let i = 1; i < 5; ++i) {
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login");
+ } else {
+ Services.obs.notifyObservers(null, "network:link-status-changed", "down");
+ }
+
+ info("notified observers");
+
+ Assert.ok(racer._aggregator.aborted);
+ ensureNoTelemetry();
+ Assert.equal(racer._retryCount, i);
+ Assert.ok(!deferred.resolved);
+
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login-success");
+ } else {
+ Services.obs.notifyObservers(null, "network:link-status-changed", "up");
+ }
+ }
+
+ // Simulate a "down" network event and ensure we still send telemetry
+ // since we've maxed out our retry count.
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login");
+ } else {
+ Services.obs.notifyObservers(null, "network:link-status-changed", "down");
+ }
+ Assert.ok(!racer._aggregator.aborted);
+ await deferred.promise;
+ Assert.equal(racer._retryCount, 5);
+
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ ).parent;
+ Assert.ok(events);
+ events = events.filter(e => e[1] == "security.doh.trrPerformance");
+ Assert.equal(events.length, racer._aggregator.totalLookups);
+
+ Services.telemetry.clearEvents();
+ if (captivePortal) {
+ Services.obs.notifyObservers(null, "captive-portal-login-abort");
+ }
+}
+
+add_task(async function test_TRRRacer_maxRetries() {
+ await test_TRRRacer_maxRetries_helper(false);
+ await test_TRRRacer_maxRetries_helper(true);
+});
+
+add_task(async function test_TRRRacer_getFastestTRRFromResults() {
+ let results = [
+ { trr: "trr1", time: 10 },
+ { trr: "trr2", time: 100 },
+ { trr: "trr1", time: 1000 },
+ { trr: "trr2", time: 110 },
+ { trr: "trr3", time: -1 },
+ { trr: "trr4", time: -1 },
+ { trr: "trr4", time: -1 },
+ { trr: "trr4", time: 1 },
+ { trr: "trr4", time: 1 },
+ { trr: "trr5", time: 10 },
+ { trr: "trr5", time: 20 },
+ { trr: "trr5", time: 1000 },
+ ];
+ let racer = new TRRRacer(undefined, trrList);
+ let fastest = racer._getFastestTRRFromResults(results);
+ // trr1's geometric mean is 100
+ // trr2's geometric mean is 110
+ // trr3 has no valid times, excluded
+ // trr4 has 50% invalid times, excluded
+ // trr5's geometric mean is ~58.5, it's the winner.
+ Assert.equal(fastest, "trr5");
+
+ // When no valid entries are available, undefined is the default output.
+ results = [
+ { trr: "trr1", time: -1 },
+ { trr: "trr2", time: -1 },
+ ];
+
+ fastest = racer._getFastestTRRFromResults(results);
+ Assert.equal(fastest, undefined);
+
+ // When passing `returnRandomDefault = true`, verify that both TRRs are
+ // possible outputs. The probability that the randomization is working
+ // correctly and we consistently get the same output after 50 iterations is
+ // 0.5^50 ~= 8.9*10^-16.
+ let firstResult = racer._getFastestTRRFromResults(results, true);
+ while (racer._getFastestTRRFromResults(results, true) == firstResult) {
+ continue;
+ }
+ Assert.ok(true, "Both TRRs were possible outputs when all results invalid.");
+});
diff --git a/browser/components/doh/test/unit/test_heuristics.js b/browser/components/doh/test/unit/test_heuristics.js
new file mode 100644
index 0000000000..a6a6c9b6c9
--- /dev/null
+++ b/browser/components/doh/test/unit/test_heuristics.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+let cid;
+
+async function SetMockParentalControlEnabled(aEnabled) {
+ if (cid) {
+ MockRegistrar.unregister(cid);
+ }
+
+ let parentalControlsService = {
+ parentalControlsEnabled: aEnabled,
+ QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]),
+ };
+ cid = MockRegistrar.register(
+ "@mozilla.org/parental-controls-service;1",
+ parentalControlsService
+ );
+}
+
+registerCleanupFunction(() => {
+ if (cid) {
+ MockRegistrar.unregister(cid);
+ }
+});
+
+add_task(setup);
+
+add_task(async function test_parentalControls() {
+ let DoHHeuristics = ChromeUtils.importESModule(
+ "resource:///modules/DoHHeuristics.sys.mjs"
+ );
+
+ let parentalControls = DoHHeuristics.parentalControls;
+
+ Assert.equal(
+ await parentalControls(),
+ "enable_doh",
+ "By default, parental controls should be disabled and doh should be enabled"
+ );
+
+ SetMockParentalControlEnabled(false);
+
+ Assert.equal(
+ await parentalControls(),
+ "enable_doh",
+ "Mocked parental controls service is disabled; doh is enabled"
+ );
+
+ SetMockParentalControlEnabled(true);
+
+ Assert.equal(
+ await parentalControls(),
+ "enable_doh",
+ "Default value of mocked parental controls service is disabled; doh is enabled"
+ );
+
+ SetMockParentalControlEnabled(false);
+
+ Assert.equal(
+ await parentalControls(),
+ "enable_doh",
+ "Mocked parental controls service is disabled; doh is enabled"
+ );
+
+ MockRegistrar.unregister(cid);
+
+ Assert.equal(
+ await parentalControls(),
+ "enable_doh",
+ "By default, parental controls should be disabled and doh should be enabled"
+ );
+});
diff --git a/browser/components/doh/test/unit/xpcshell.toml b/browser/components/doh/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..9d40b74621
--- /dev/null
+++ b/browser/components/doh/test/unit/xpcshell.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+skip-if = ["os == 'android'"] # bug 1730213
+head = "head.js"
+firefox-appdir = "browser"
+support-files = ["../../../../../netwerk/test/unit/http2-ca.pem"]
+
+["test_DNSLookup.js"]
+skip-if = ["debug"] # Bug 1617845
+
+["test_LookupAggregator.js"]
+
+["test_TRRRacer.js"]
+
+["test_heuristics.js"]