"use strict";

/* import-globals-from trr_common.js */
/* import-globals-from head_trr.js */

const gDefaultPref = Services.prefs.getDefaultBranch("");

const { NodeServer } = ChromeUtils.importESModule(
  "resource://testing-common/NodeServer.sys.mjs"
);

SetParentalControlEnabled(false);

let trrServer;
add_setup(async function setup() {
  Services.prefs.setBoolPref("network.dns.get-ttl", false);
  trr_test_setup();
  trrServer = new TRRServer();
  await trrServer.start();
  h2Port = trrServer.port();

  registerCleanupFunction(async () => {
    trr_clear_prefs();
    Services.prefs.clearUserPref("network.dns.get-ttl");
    Services.prefs.clearUserPref("network.dns.disableIPv6");
    if (trrServer) {
      await trrServer.stop();
    }
  });
});

async function waitForConfirmation(expectedResponseIP, confirmationShouldFail) {
  // Check that the confirmation eventually completes.
  let count = 100;
  while (count > 0) {
    if (count == 50 || count == 10) {
      // At these two points we do a longer timeout to account for a slow
      // response on the server side. This is usually a problem on the Android
      // because of the increased delay between the emulator and host.
      await new Promise(resolve => do_timeout(100 * (100 / count), resolve));
    }
    let { inRecord } = await new TRRDNSListener(
      `ip${count}.example.org`,
      undefined,
      false
    );
    inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
    let responseIP = inRecord.getNextAddrAsString();
    Assert.ok(true, responseIP);
    if (responseIP == expectedResponseIP) {
      break;
    }
    count--;
  }

  if (confirmationShouldFail) {
    Assert.equal(count, 0, "Confirmation did not finish after 100 iterations");
    return;
  }

  Assert.greater(count, 0, "Finished confirmation before 100 iterations");
}

function setModeAndURI(mode, path) {
  Services.prefs.setIntPref("network.trr.mode", mode);
  Services.prefs.setCharPref(
    "network.trr.uri",
    `https://${TRR_Domain}:${h2Port}/${path}`
  );
}

function makeChan(url, mode) {
  let chan = NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);
  chan.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
  chan.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
  chan.setTRRMode(mode);
  return chan;
}

add_task(async function test_server_up() {
  // This test checks that moz-http2.js running in node is working.
  // This should always be the first test in this file (except for setup)
  // otherwise we may encounter random failures when the http2 server is down.

  await NodeServer.execute("bad_id", `"hello"`)
    .then(() => ok(false, "expecting to throw"))
    .catch(e => equal(e.message, "Error: could not find id"));
});

add_task(async function test_trr_flags() {
  Services.prefs.setBoolPref("network.trr.fallback-on-zero-response", true);
  Services.prefs.setIntPref("network.trr.request_timeout_ms", 10000);
  Services.prefs.setIntPref(
    "network.trr.request_timeout_mode_trronly_ms",
    10000
  );

  let httpserv = new HttpServer();
  httpserv.registerPathHandler("/", function handler(metadata, response) {
    let content = "ok";
    response.setHeader("Content-Length", `${content.length}`);
    response.bodyOutputStream.write(content, content.length);
  });
  httpserv.start(-1);

  const URL = `http://example.com:${httpserv.identity.primaryPort}/`;

  for (let mode of [0, 1, 2, 3, 4, 5]) {
    setModeAndURI(mode, "doh?responseIP=127.0.0.1");
    for (let flag of [
      Ci.nsIRequest.TRR_DEFAULT_MODE,
      Ci.nsIRequest.TRR_DISABLED_MODE,
      Ci.nsIRequest.TRR_FIRST_MODE,
      Ci.nsIRequest.TRR_ONLY_MODE,
    ]) {
      Services.dns.clearCache(true);
      let chan = makeChan(URL, flag);
      let expectTRR =
        ([2, 3].includes(mode) && flag != Ci.nsIRequest.TRR_DISABLED_MODE) ||
        (mode == 0 &&
          [Ci.nsIRequest.TRR_FIRST_MODE, Ci.nsIRequest.TRR_ONLY_MODE].includes(
            flag
          ));

      await new Promise(resolve =>
        chan.asyncOpen(new ChannelListener(resolve))
      );

      equal(chan.getTRRMode(), flag);
      equal(
        expectTRR,
        chan.QueryInterface(Ci.nsIHttpChannelInternal).isResolvedByTRR
      );
    }
  }

  await new Promise(resolve => httpserv.stop(resolve));
  Services.prefs.clearUserPref("network.trr.fallback-on-zero-response");
  Services.prefs.clearUserPref("network.trr.request_timeout_ms");
  Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms");
});

add_task(test_A_record);

add_task(test_AAAA_records);

add_task(test_RFC1918);

add_task(test_GET_ECS);

add_task(test_timeout_mode3);

add_task(test_trr_retry);

add_task(test_strict_native_fallback);

add_task(test_no_answers_fallback);

add_task(test_404_fallback);

add_task(test_mode_1_and_4);

add_task(test_CNAME);

add_task(test_name_mismatch);

add_task(test_mode_2);

add_task(test_excluded_domains);

add_task(test_captiveportal_canonicalURL);

add_task(test_parentalcontrols);

// TRR-first check that DNS result is used if domain is part of the builtin-excluded-domains pref
add_task(test_builtin_excluded_domains);

add_task(test_excluded_domains_mode3);

add_task(test25e);

add_task(test_parentalcontrols_mode3);

add_task(test_builtin_excluded_domains_mode3);

add_task(count_cookies);

add_task(test_connection_closed);

add_task(async function test_clearCacheOnURIChange() {
  info("Check that the TRR cache should be cleared by a pref change.");
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true);
  setModeAndURI(2, "doh?responseIP=7.7.7.7");

  await new TRRDNSListener("bar.example.com", "7.7.7.7");

  // The TRR cache should be cleared by this pref change.
  Services.prefs.setCharPref(
    "network.trr.uri",
    `https://localhost:${h2Port}/doh?responseIP=8.8.8.8`
  );

  await new TRRDNSListener("bar.example.com", "8.8.8.8");
  Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
});

add_task(async function test_dnsSuffix() {
  info("Checking that domains matching dns suffix list use Do53");
  // Preserve historical behavior of this task: DNS-suffix domains are
  // excluded from TRR in mode 3 as well. The new behavior is exercised by
  // test_dnsSuffix_mode3_pref below.
  Services.prefs.setBoolPref(
    "network.trr.exclude_dns_suffix_in_mode_trronly",
    true
  );
  async function checkDnsSuffixInMode(mode) {
    Services.dns.clearCache(true);
    setModeAndURI(mode, "doh?responseIP=1.2.3.4");
    await new TRRDNSListener("example.org", "1.2.3.4");
    await new TRRDNSListener("test.com", "1.2.3.4");

    let networkLinkService = {
      dnsSuffixList: ["example.org"],
      QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
    };
    Services.obs.notifyObservers(
      networkLinkService,
      "network:dns-suffix-list-updated"
    );
    await new TRRDNSListener("test.com", "1.2.3.4");
    if (Services.prefs.getBoolPref("network.trr.split_horizon_mitigations")) {
      await new TRRDNSListener("example.org", "127.0.0.1");
    } else {
      await new TRRDNSListener("example.org", "1.2.3.4");
    }

    // Attempt to clean up, just in case
    networkLinkService.dnsSuffixList = [];
    Services.obs.notifyObservers(
      networkLinkService,
      "network:dns-suffix-list-updated"
    );
  }

  Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", true);
  await checkDnsSuffixInMode(2);
  Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");
  await checkDnsSuffixInMode(3);
  Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", false);
  // Test again with mitigations off
  await checkDnsSuffixInMode(2);
  await checkDnsSuffixInMode(3);
  Services.prefs.clearUserPref("network.trr.split_horizon_mitigations");
  Services.prefs.clearUserPref("network.trr.bootstrapAddr");
  Services.prefs.clearUserPref(
    "network.trr.exclude_dns_suffix_in_mode_trronly"
  );
});

add_task(async function test_dnsSuffix_mode3_pref() {
  info(
    "In TRR-only mode the DNS suffix list should be ignored when " +
      "network.trr.exclude_dns_suffix_in_mode_trronly is false (default)."
  );
  Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", true);
  Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");

  let networkLinkService = {
    dnsSuffixList: ["example.org"],
    QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
  };

  async function checkMode3(prefValue, expectedIP) {
    Services.prefs.setBoolPref(
      "network.trr.exclude_dns_suffix_in_mode_trronly",
      prefValue
    );
    Services.dns.clearCache(true);
    setModeAndURI(3, "doh?responseIP=1.2.3.4");
    Services.obs.notifyObservers(
      networkLinkService,
      "network:dns-suffix-list-updated"
    );
    await new TRRDNSListener("example.org", expectedIP);
  }

  // Default behavior: pref is false, suffix is ignored in mode 3, so the
  // host is resolved via TRR.
  await checkMode3(false, "1.2.3.4");
  // Opt-in to old behavior: pref is true, suffix is excluded so the local
  // bootstrap resolver answers with 127.0.0.1.
  await checkMode3(true, "127.0.0.1");

  networkLinkService.dnsSuffixList = [];
  Services.obs.notifyObservers(
    networkLinkService,
    "network:dns-suffix-list-updated"
  );
  Services.prefs.clearUserPref(
    "network.trr.exclude_dns_suffix_in_mode_trronly"
  );
  Services.prefs.clearUserPref("network.trr.split_horizon_mitigations");
  Services.prefs.clearUserPref("network.trr.bootstrapAddr");
});

add_task(async function test_dnsSuffix_request_mode_trronly() {
  info(
    "A per-request TRR mode of TRR_ONLY_MODE should also cause the DNS " +
      "suffix list to be ignored by default, even when the global mode is " +
      "TRR-first."
  );
  Services.prefs.setBoolPref("network.trr.split_horizon_mitigations", true);
  Services.prefs.setCharPref("network.trr.bootstrapAddr", "127.0.0.1");

  let networkLinkService = {
    dnsSuffixList: ["example.org"],
    QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
  };

  // TRR_ONLY_MODE encoded into resolve flags. See RESOLVE_TRR_MODE_MASK in
  // nsIDNSService.idl: the request TRR mode lives in bits 11/12, so
  // TRR_ONLY_MODE (=3) corresponds to (3 << 11).
  const TRR_ONLY_FLAGS = 3 << 11;

  async function checkRequestModeTRROnly(prefValue, expectedIP) {
    Services.prefs.setBoolPref(
      "network.trr.exclude_dns_suffix_in_mode_trronly",
      prefValue
    );
    Services.dns.clearCache(true);
    // Global mode is 2 (TRR-first); the per-request override drives behavior.
    setModeAndURI(2, "doh?responseIP=1.2.3.4");
    Services.obs.notifyObservers(
      networkLinkService,
      "network:dns-suffix-list-updated"
    );
    await new TRRDNSListener("example.org", {
      expectedAnswer: expectedIP,
      flags: TRR_ONLY_FLAGS,
    });
  }

  // Default: pref is false → request-mode TRR_ONLY ignores the suffix.
  await checkRequestModeTRROnly(false, "1.2.3.4");
  // Opt-in: pref is true → suffix is honored even for TRR_ONLY requests.
  await checkRequestModeTRROnly(true, "127.0.0.1");

  networkLinkService.dnsSuffixList = [];
  Services.obs.notifyObservers(
    networkLinkService,
    "network:dns-suffix-list-updated"
  );
  Services.prefs.clearUserPref(
    "network.trr.exclude_dns_suffix_in_mode_trronly"
  );
  Services.prefs.clearUserPref("network.trr.split_horizon_mitigations");
  Services.prefs.clearUserPref("network.trr.bootstrapAddr");
});

add_task(async function test_async_resolve_with_trr_server() {
  info("Checking asyncResolveWithTrrServer");
  Services.dns.clearCache(true);
  Services.prefs.setIntPref("network.trr.mode", 0); // TRR-disabled

  await new TRRDNSListener(
    "bar_with_trr1.example.com",
    "2.2.2.2",
    true,
    undefined,
    `https://foo.example.com:${h2Port}/doh?responseIP=2.2.2.2`
  );

  // Test request without trr server, it should return a native dns response.
  await new TRRDNSListener("bar_with_trr1.example.com", "127.0.0.1");

  // Mode 2
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");

  await new TRRDNSListener(
    "bar_with_trr2.example.com",
    "3.3.3.3",
    true,
    undefined,
    `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
  );

  // Test request without trr server, it should return a response from trr server defined in the pref.
  await new TRRDNSListener("bar_with_trr2.example.com", "2.2.2.2");

  // Mode 3
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=2.2.2.2");

  await new TRRDNSListener(
    "bar_with_trr3.example.com",
    "3.3.3.3",
    true,
    undefined,
    `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
  );

  // Test request without trr server, it should return a response from trr server defined in the pref.
  await new TRRDNSListener("bar_with_trr3.example.com", "2.2.2.2");

  // Mode 5
  Services.dns.clearCache(true);
  setModeAndURI(5, "doh?responseIP=2.2.2.2");

  // When dns is resolved in socket process, we can't set |expectEarlyFail| to true.
  let inSocketProcess = mozinfo.socketprocess_networking;
  await new TRRDNSListener(
    "bar_with_trr3.example.com",
    undefined,
    false,
    undefined,
    `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`,
    !inSocketProcess
  );

  // Call normal AsyncOpen, it will return result from the native resolver.
  await new TRRDNSListener("bar_with_trr3.example.com", "127.0.0.1");

  // Check that cache is ignored when server is different
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=2.2.2.2");

  await new TRRDNSListener("bar_with_trr4.example.com", "2.2.2.2", true);

  // The record will be fetch again.
  await new TRRDNSListener(
    "bar_with_trr4.example.com",
    "3.3.3.3",
    true,
    undefined,
    `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
  );

  // The record will be fetch again.
  await new TRRDNSListener(
    "bar_with_trr5.example.com",
    "4.4.4.4",
    true,
    undefined,
    `https://foo.example.com:${h2Port}/doh?responseIP=4.4.4.4`
  );

  // Check no fallback and no blocklisting upon failure
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");

  let { inStatus } = await new TRRDNSListener(
    "bar_with_trr6.example.com",
    undefined,
    false,
    undefined,
    `https://foo.example.com:${h2Port}/404`
  );
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );

  await new TRRDNSListener("bar_with_trr6.example.com", "2.2.2.2", true);

  // Check that DoH push doesn't work
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");

  await new TRRDNSListener(
    "bar_with_trr7.example.com",
    "3.3.3.3",
    true,
    undefined,
    `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
  );

  // Check confirmation is ignored
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=1::ffff");
  Services.prefs.clearUserPref("network.trr.useGET");
  Services.prefs.clearUserPref("network.trr.disable-ECS");
  Services.prefs.setCharPref(
    "network.trr.confirmationNS",
    "confirm.example.com"
  );

  // AsyncResoleWithTrrServer will succeed
  await new TRRDNSListener(
    "bar_with_trr8.example.com",
    "3.3.3.3",
    true,
    undefined,
    `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
  );

  Services.prefs.setCharPref("network.trr.confirmationNS", "skip");

  // Bad port
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=2.2.2.2");

  ({ inStatus } = await new TRRDNSListener(
    "only_once.example.com",
    undefined,
    false,
    undefined,
    `https://target.example.com:666/404`
  ));
  Assert.ok(
    !Components.isSuccessCode(inStatus),
    `${inStatus} should be an error code`
  );

  // // MOZ_LOG=sync,timestamp,nsHostResolver:5 We should not keep resolving only_once.example.com
  // // TODO: find a way of automating this
  // await new Promise(resolve => {});
});

add_task(test_fetch_time);

add_task(async function test_content_encoding_gzip() {
  info("Checking gzip content encoding");
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref(
    "network.trr.send_empty_accept-encoding_headers",
    false
  );
  setModeAndURI(3, "doh?responseIP=2.2.2.2");

  await new TRRDNSListener("bar.example.com", "2.2.2.2");
  Services.prefs.clearUserPref(
    "network.trr.send_empty_accept-encoding_headers"
  );
});

add_task(async function test_redirect() {
  info("Check handling of redirect");

  // GET
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?redirect=4.4.4.4{&dns}");
  Services.prefs.setBoolPref("network.trr.useGET", true);
  Services.prefs.setBoolPref("network.trr.disable-ECS", true);

  await new TRRDNSListener("ecs.example.com", "4.4.4.4");

  // POST
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.useGET", false);
  setModeAndURI(3, "doh?redirect=4.4.4.4");

  await new TRRDNSListener("bar.example.com", "4.4.4.4");

  Services.prefs.clearUserPref("network.trr.useGET");
  Services.prefs.clearUserPref("network.trr.disable-ECS");
});

// confirmationNS set without confirmed NS yet
// checks that we properly fall back to DNS is confirmation is not ready yet,
// and wait-for-confirmation pref is true
add_task(async function test_confirmation() {
  info("Checking that we fall back correctly when confirmation is pending");
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.wait-for-confirmation", true);
  setModeAndURI(2, "doh?responseIP=7.7.7.7&slowConfirm=true");
  Services.prefs.setCharPref(
    "network.trr.confirmationNS",
    "confirm.example.com"
  );

  await new TRRDNSListener("example.org", "127.0.0.1");
  await new Promise(resolve => do_timeout(1000, resolve));
  await waitForConfirmation("7.7.7.7");

  // Reset between each test to force re-confirm
  Services.prefs.setCharPref("network.trr.confirmationNS", "skip");

  info("Check that confirmation is skipped in mode 3");
  // This is just a smoke test to make sure lookups succeed immediately
  // in mode 3 without waiting for confirmation.
  Services.dns.clearCache(true);
  setModeAndURI(3, "doh?responseIP=1::ffff&slowConfirm=true");
  Services.prefs.setCharPref(
    "network.trr.confirmationNS",
    "confirm.example.com"
  );

  await new TRRDNSListener("skipConfirmationForMode3.example.com", "1::ffff");

  // Reset between each test to force re-confirm
  Services.prefs.setCharPref("network.trr.confirmationNS", "skip");

  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.wait-for-confirmation", false);
  setModeAndURI(2, "doh?responseIP=7.7.7.7&slowConfirm=true");
  Services.prefs.setCharPref(
    "network.trr.confirmationNS",
    "confirm.example.com"
  );

  // DoH available immediately
  await new TRRDNSListener("example.org", "7.7.7.7");

  // Reset between each test to force re-confirm
  Services.prefs.setCharPref("network.trr.confirmationNS", "skip");

  // Fallback when confirmation fails
  Services.dns.clearCache(true);
  Services.prefs.setBoolPref("network.trr.wait-for-confirmation", true);
  setModeAndURI(2, "404");
  Services.prefs.setCharPref(
    "network.trr.confirmationNS",
    "confirm.example.com"
  );

  await waitForConfirmation("7.7.7.7", true);

  await new TRRDNSListener("example.org", "127.0.0.1");

  // Reset
  Services.prefs.setCharPref("network.trr.confirmationNS", "skip");
  Services.prefs.clearUserPref("network.trr.wait-for-confirmation");
});

add_task(test_fqdn);

add_task(async function test_detected_uri() {
  info("Test setDetectedTrrURI");
  Services.dns.clearCache(true);
  Services.prefs.setIntPref("network.trr.mode", 3);
  Services.prefs.clearUserPref("network.trr.uri");
  let defaultURI = gDefaultPref.getCharPref("network.trr.default_provider_uri");
  gDefaultPref.setCharPref(
    "network.trr.default_provider_uri",
    `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6`
  );
  await new TRRDNSListener("domainA.example.org.", "3.4.5.6");
  Services.dns.setDetectedTrrURI(
    `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
  );
  await new TRRDNSListener("domainB.example.org.", "1.2.3.4");
  gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI);

  // With a user-set doh uri this time.
  Services.dns.clearCache(true);
  setModeAndURI(2, "doh?responseIP=4.5.6.7");
  await new TRRDNSListener("domainA.example.org.", "4.5.6.7");

  // This should be a no-op, since we have a user-set URI
  Services.dns.setDetectedTrrURI(
    `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
  );
  await new TRRDNSListener("domainB.example.org.", "4.5.6.7");

  // Test network link status change
  Services.dns.clearCache(true);
  Services.prefs.setIntPref("network.trr.mode", 3);
  Services.prefs.clearUserPref("network.trr.uri");
  gDefaultPref.setCharPref(
    "network.trr.default_provider_uri",
    `https://foo.example.com:${h2Port}/doh?responseIP=3.4.5.6`
  );
  await new TRRDNSListener("domainA.example.org.", "3.4.5.6");
  Services.dns.setDetectedTrrURI(
    `https://foo.example.com:${h2Port}/doh?responseIP=1.2.3.4`
  );
  await new TRRDNSListener("domainB.example.org.", "1.2.3.4");

  let networkLinkService = {
    platformDNSIndications: 0,
    QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
  };

  Services.obs.notifyObservers(
    networkLinkService,
    "network:link-status-changed",
    "changed"
  );

  await new TRRDNSListener("domainC.example.org.", "3.4.5.6");

  gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI);
});

add_task(async function test_pref_changes() {
  info("Testing pref change handling");
  Services.prefs.clearUserPref("network.trr.uri");
  let defaultURI = gDefaultPref.getCharPref("network.trr.default_provider_uri");

  async function doThenCheckURI(closure, expectedURI, expectChange = true) {
    let uriChanged;
    if (expectChange) {
      uriChanged = topicObserved("network:trr-uri-changed");
    }
    closure();
    if (expectChange) {
      await uriChanged;
    }
    equal(Services.dns.currentTrrURI, expectedURI);
  }

  // setting the default value of the pref should be reflected in the URI
  await doThenCheckURI(() => {
    gDefaultPref.setCharPref(
      "network.trr.default_provider_uri",
      `https://foo.example.com:${h2Port}/doh?default`
    );
  }, `https://foo.example.com:${h2Port}/doh?default`);

  // the user set value should be reflected in the URI
  await doThenCheckURI(() => {
    Services.prefs.setCharPref(
      "network.trr.uri",
      `https://foo.example.com:${h2Port}/doh?user`
    );
  }, `https://foo.example.com:${h2Port}/doh?user`);

  // A user set pref is selected, so it should be chosen instead of the rollout
  await doThenCheckURI(
    () => {
      Services.prefs.setCharPref(
        "doh-rollout.uri",
        `https://foo.example.com:${h2Port}/doh?rollout`
      );
    },
    `https://foo.example.com:${h2Port}/doh?user`,
    false
  );

  // There is no user set pref, so we go to the rollout pref
  await doThenCheckURI(() => {
    Services.prefs.clearUserPref("network.trr.uri");
  }, `https://foo.example.com:${h2Port}/doh?rollout`);

  // When the URI is set by the rollout addon, detection is allowed
  await doThenCheckURI(() => {
    Services.dns.setDetectedTrrURI(
      `https://foo.example.com:${h2Port}/doh?detected`
    );
  }, `https://foo.example.com:${h2Port}/doh?detected`);

  // Should switch back to the default provided by the rollout addon
  await doThenCheckURI(() => {
    let networkLinkService = {
      platformDNSIndications: 0,
      QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
    };
    Services.obs.notifyObservers(
      networkLinkService,
      "network:link-status-changed",
      "changed"
    );
  }, `https://foo.example.com:${h2Port}/doh?rollout`);

  // Again the user set pref should be chosen
  await doThenCheckURI(() => {
    Services.prefs.setCharPref(
      "network.trr.uri",
      `https://foo.example.com:${h2Port}/doh?user`
    );
  }, `https://foo.example.com:${h2Port}/doh?user`);

  // Detection should not work with a user set pref
  await doThenCheckURI(
    () => {
      Services.dns.setDetectedTrrURI(
        `https://foo.example.com:${h2Port}/doh?detected`
      );
    },
    `https://foo.example.com:${h2Port}/doh?user`,
    false
  );

  // Should stay the same on network changes
  await doThenCheckURI(
    () => {
      let networkLinkService = {
        platformDNSIndications: 0,
        QueryInterface: ChromeUtils.generateQI(["nsINetworkLinkService"]),
      };
      Services.obs.notifyObservers(
        networkLinkService,
        "network:link-status-changed",
        "changed"
      );
    },
    `https://foo.example.com:${h2Port}/doh?user`,
    false
  );

  // Restore the pref
  gDefaultPref.setCharPref("network.trr.default_provider_uri", defaultURI);
});

add_task(async function test_dohrollout_mode() {
  info("Testing doh-rollout.mode");
  Services.prefs.clearUserPref("network.trr.mode");
  Services.prefs.clearUserPref("doh-rollout.mode");

  equal(Services.dns.currentTrrMode, 0);

  async function doThenCheckMode(trrMode, rolloutMode, expectedMode, message) {
    let modeChanged;
    if (Services.dns.currentTrrMode != expectedMode) {
      modeChanged = topicObserved("network:trr-mode-changed");
    }

    if (trrMode != undefined) {
      Services.prefs.setIntPref("network.trr.mode", trrMode);
    }

    if (rolloutMode != undefined) {
      Services.prefs.setIntPref("doh-rollout.mode", rolloutMode);
    }

    if (modeChanged) {
      await modeChanged;
    }
    equal(Services.dns.currentTrrMode, expectedMode, message);
  }

  await doThenCheckMode(2, undefined, 2);
  await doThenCheckMode(3, undefined, 3);
  await doThenCheckMode(5, undefined, 5);
  await doThenCheckMode(2, undefined, 2);
  await doThenCheckMode(0, undefined, 0);
  await doThenCheckMode(1, undefined, 5);
  await doThenCheckMode(6, undefined, 5);

  await doThenCheckMode(2, 0, 2);
  await doThenCheckMode(2, 1, 2);
  await doThenCheckMode(2, 2, 2);
  await doThenCheckMode(2, 3, 2);
  await doThenCheckMode(2, 5, 2);
  await doThenCheckMode(3, 2, 3);
  await doThenCheckMode(5, 2, 5);

  Services.prefs.clearUserPref("network.trr.mode");
  Services.prefs.clearUserPref("doh-rollout.mode");

  await doThenCheckMode(undefined, 2, 2);
  await doThenCheckMode(undefined, 3, 3);

  // All modes that are not 0,2,3 are treated as 5
  await doThenCheckMode(undefined, 5, 5);
  await doThenCheckMode(undefined, 4, 5);
  await doThenCheckMode(undefined, 6, 5);

  await doThenCheckMode(undefined, 2, 2);
  await doThenCheckMode(3, undefined, 3);

  Services.prefs.clearUserPref("network.trr.mode");
  equal(Services.dns.currentTrrMode, 2);
  Services.prefs.clearUserPref("doh-rollout.mode");
  equal(Services.dns.currentTrrMode, 0);
});

add_task(test_ipv6_trr_fallback);

add_task(test_ipv4_trr_fallback);

add_task(test_no_retry_without_doh);

// This test checks that normally when the TRR mode goes from ON -> OFF
// we purge the DNS cache (including TRR), so the entries aren't used on
// networks where they shouldn't. For example - turning on a VPN.
add_task(async function test_purge_trr_cache_on_mode_change() {
  info("Checking that we purge cache when TRR is turned off");
  Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", true);

  Services.prefs.setIntPref("network.trr.mode", 0);
  Services.prefs.setIntPref("doh-rollout.mode", 2);
  Services.prefs.setCharPref(
    "network.trr.uri",
    `https://foo.example.com:${h2Port}/doh?responseIP=3.3.3.3`
  );

  await new TRRDNSListener("cached.example.com", "3.3.3.3");
  Services.prefs.clearUserPref("doh-rollout.mode");

  await new TRRDNSListener("cached.example.com", "127.0.0.1");

  Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false);
  Services.prefs.clearUserPref("doh-rollout.mode");
});

add_task(async function test_old_bootstrap_pref() {
  Services.dns.clearCache(true);
  // Note this is a remote address. Setting this pref should have no effect,
  // as this is the old name for the bootstrap pref.
  // If this were to be used, the test would crash when accessing a non-local
  // IP address.
  Services.prefs.setCharPref("network.trr.bootstrapAddress", "1.1.1.1");
  setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh?responseIP=4.4.4.4`);
  await new TRRDNSListener("testytest.com", "4.4.4.4");
});

add_task(async function test_padding() {
  setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh`);
  async function CheckPadding(
    pad_length,
    request,
    none,
    ecs,
    padding,
    ecsPadding
  ) {
    Services.prefs.setIntPref("network.trr.padding.length", pad_length);
    Services.dns.clearCache(true);
    Services.prefs.setBoolPref("network.trr.padding", false);
    Services.prefs.setBoolPref("network.trr.disable-ECS", false);
    await new TRRDNSListener(request, none);

    Services.dns.clearCache(true);
    Services.prefs.setBoolPref("network.trr.padding", false);
    Services.prefs.setBoolPref("network.trr.disable-ECS", true);
    await new TRRDNSListener(request, ecs);

    Services.dns.clearCache(true);
    Services.prefs.setBoolPref("network.trr.padding", true);
    Services.prefs.setBoolPref("network.trr.disable-ECS", false);
    await new TRRDNSListener(request, padding);

    Services.dns.clearCache(true);
    Services.prefs.setBoolPref("network.trr.padding", true);
    Services.prefs.setBoolPref("network.trr.disable-ECS", true);
    await new TRRDNSListener(request, ecsPadding);
  }

  // short domain name
  await CheckPadding(
    16,
    "a.pd",
    "2.2.0.22",
    "2.2.0.41",
    "1.1.0.48",
    "1.1.0.48"
  );
  await CheckPadding(256, "a.pd", "2.2.0.22", "2.2.0.41", "1.1.1.0", "1.1.1.0");

  // medium domain name
  await CheckPadding(
    16,
    "has-padding.pd",
    "2.2.0.32",
    "2.2.0.51",
    "1.1.0.48",
    "1.1.0.64"
  );
  await CheckPadding(
    128,
    "has-padding.pd",
    "2.2.0.32",
    "2.2.0.51",
    "1.1.0.128",
    "1.1.0.128"
  );
  await CheckPadding(
    80,
    "has-padding.pd",
    "2.2.0.32",
    "2.2.0.51",
    "1.1.0.80",
    "1.1.0.80"
  );

  // long domain name
  await CheckPadding(
    16,
    "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd",
    "2.2.0.131",
    "2.2.0.150",
    "1.1.0.160",
    "1.1.0.160"
  );
  await CheckPadding(
    128,
    "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd",
    "2.2.0.131",
    "2.2.0.150",
    "1.1.1.0",
    "1.1.1.0"
  );
  await CheckPadding(
    80,
    "abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.abcdefghijklmnopqrstuvwxyz0123456789.pd",
    "2.2.0.131",
    "2.2.0.150",
    "1.1.0.160",
    "1.1.0.160"
  );
});

// Can't test for socket process since telemetry is captured in different process.
add_task(
  { skip_if: () => mozinfo.socketprocess_networking },
  async function test_trr_pb_telemetry() {
    setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh`);
    Services.dns.clearCache(true);
    Services.fog.initializeFOG();
    Services.fog.testResetFOG();
    await new TRRDNSListener("testytest.com", { expectedAnswer: "5.5.5.5" });

    Assert.equal(
      await Glean.networking.trrRequestCount.regular.testGetValue(),
      2
    ); // One for IPv4 and one for IPv6.
    Assert.equal(
      await Glean.networking.trrRequestCount.private.testGetValue(),
      null
    );

    await new TRRDNSListener("testytest.com", {
      expectedAnswer: "5.5.5.5",
      originAttributes: { privateBrowsingId: 1 },
    });

    Assert.equal(
      await Glean.networking.trrRequestCount.regular.testGetValue(),
      2
    );
    Assert.equal(
      await Glean.networking.trrRequestCount.private.testGetValue(),
      2
    );
    // We've made 4 TRR requests.
    Assert.equal(
      await Glean.networking.trrRequestSize.other.testGetValue().count,
      4
    );
    Assert.equal(
      await Glean.networking.trrResponseSize.other.testGetValue().count,
      4
    );
  }
);

add_task(
  { skip_if: () => mozinfo.socketprocess_networking },
  async function test_trr_timing_telemetry() {
    setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh`);
    Services.dns.clearCache(true);

    // Close the previous TRR connection.
    Services.obs.notifyObservers(null, "net:cancel-all-connections");
    await new Promise(r => do_timeout(3000, r));

    Services.fog.testResetFOG();
    // Disable IPv6, so we only send one TRR request.
    Services.prefs.setBoolPref("network.dns.disableIPv6", true);
    await new TRRDNSListener("timing.com", { expectedAnswer: "5.5.5.5" });

    await new Promise(r => do_timeout(100, r));

    let dnsStart = await Glean.networking.trrDnsStart.other.testGetValue();
    let dnsEnd = await Glean.networking.trrDnsEnd.other.testGetValue();
    let tcpConnection =
      await Glean.networking.trrTcpConnection.other.testGetValue();
    let tlsHandshake =
      await Glean.networking.trrTlsHandshake.other.testGetValue();
    let openToFirstSent =
      await Glean.networking.trrOpenToFirstSent.other.testGetValue();
    let firstSentToLastReceived =
      await Glean.networking.trrFirstSentToLastReceived.other.testGetValue();
    let openToFirstReceived =
      await Glean.networking.trrOpenToFirstReceived.other.testGetValue();
    let completeLoad =
      await Glean.networking.trrCompleteLoad.other.testGetValue();

    info("dnsStart=" + JSON.stringify(dnsStart));
    info("dnsEnd=" + JSON.stringify(dnsEnd));
    info("tcpConnection=" + JSON.stringify(tcpConnection));
    info("tlsHandshake=" + JSON.stringify(tlsHandshake));
    info("openToFirstSent=" + JSON.stringify(openToFirstSent));
    info("firstSentToLastReceived=" + JSON.stringify(firstSentToLastReceived));
    info("openToFirstReceived=" + JSON.stringify(openToFirstReceived));
    info("completeLoad=" + JSON.stringify(completeLoad));

    Assert.equal(dnsStart.count, 1);
    Assert.equal(dnsEnd.count, 1);
    Assert.equal(tcpConnection.count, 1);
    Assert.equal(tlsHandshake.count, 1);
    Assert.equal(openToFirstSent.count, 1);
    Assert.equal(firstSentToLastReceived.count, 1);
    Assert.equal(openToFirstReceived.count, 1);
    Assert.equal(completeLoad.count, 1);

    function getValue(obj) {
      const keys = Object.keys(obj);
      return keys.length ? +keys[0] : 0;
    }
    Assert.greaterOrEqual(
      getValue(openToFirstReceived.values),
      getValue(openToFirstSent.values),
      "openToFirstReceived >= openToFirstSent"
    );
    Assert.greaterOrEqual(
      getValue(completeLoad.values),
      getValue(openToFirstReceived.values),
      "completeLoad >= openToFirstReceived"
    );
  }
);

add_task(
  { skip_if: () => mozinfo.socketprocess_networking },
  async function test_trr_request_per_conn_telemetry() {
    setModeAndURI(Ci.nsIDNSService.MODE_TRRONLY, `doh`);
    Services.dns.clearCache(true);

    // Close the previous TRR connection.
    Services.obs.notifyObservers(null, "net:cancel-all-connections");
    await new Promise(r => do_timeout(3000, r));

    Services.fog.testResetFOG();
    Services.prefs.setBoolPref("network.dns.disableIPv6", false);
    await new TRRDNSListener("timing.com", { expectedAnswer: "5.5.5.5" });

    // Close the TRR connection again, so trr_request_count_per_conn
    // can be recorded.
    Services.obs.notifyObservers(null, "net:cancel-all-connections");
    await new Promise(r => do_timeout(3000, r));

    let requestPerConn =
      await Glean.networking.trrRequestCountPerConn.other.testGetValue();

    info("requestPerConn=" + JSON.stringify(requestPerConn));

    Assert.greaterOrEqual(requestPerConn, 2);
  }
);
