diff options
Diffstat (limited to 'browser/components/doh/test/unit')
-rw-r--r-- | browser/components/doh/test/unit/head.js | 99 | ||||
-rw-r--r-- | browser/components/doh/test/unit/test_DNSLookup.js | 62 | ||||
-rw-r--r-- | browser/components/doh/test/unit/test_LookupAggregator.js | 162 | ||||
-rw-r--r-- | browser/components/doh/test/unit/test_TRRRacer.js | 209 | ||||
-rw-r--r-- | browser/components/doh/test/unit/test_heuristics.js | 80 | ||||
-rw-r--r-- | browser/components/doh/test/unit/xpcshell.toml | 14 |
6 files changed, 626 insertions, 0 deletions
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"] |