// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/dns/dns_response_result_extractor.h"

#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/ranges/algorithm.h"
#include "base/strings/string_piece.h"
#include "base/test/simple_test_clock.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/time/time.h"
#include "net/base/connection_endpoint_metadata_test_util.h"
#include "net/base/host_port_pair.h"
#include "net/base/ip_address.h"
#include "net/base/ip_endpoint.h"
#include "net/base/net_errors.h"
#include "net/dns/dns_query.h"
#include "net/dns/dns_response.h"
#include "net/dns/dns_test_util.h"
#include "net/dns/host_cache.h"
#include "net/dns/host_resolver_internal_result.h"
#include "net/dns/host_resolver_internal_result_test_util.h"
#include "net/dns/host_resolver_results_test_util.h"
#include "net/dns/public/dns_protocol.h"
#include "net/dns/public/dns_query_type.h"
#include "net/test/gtest_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {
namespace {

using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Ne;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::Pointee;
using ::testing::ResultOf;
using ::testing::SizeIs;
using ::testing::UnorderedElementsAre;

using ExtractionError = DnsResponseResultExtractor::ExtractionError;
using ResultsOrError = DnsResponseResultExtractor::ResultsOrError;

constexpr HostResolverInternalResult::Source kDnsSource =
    HostResolverInternalResult::Source::kDns;

class DnsResponseResultExtractorTest : public ::testing::Test {
 protected:
  base::SimpleTestClock clock_;
  base::SimpleTestTickClock tick_clock_;
};

TEST_F(DnsResponseResultExtractorTest, ExtractsSingleARecord) {
  constexpr char kName[] = "address.test";
  const IPAddress kExpected(192, 168, 0, 1);

  DnsResponse response = BuildTestDnsAddressResponse(kName, kExpected);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::A, kDnsSource,
                  /*expiration_matcher=*/Ne(std::nullopt),
                  /*timed_expiration_matcher=*/Ne(std::nullopt),
                  ElementsAre(IPEndPoint(kExpected, /*port=*/0))))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsSingleAAAARecord) {
  constexpr char kName[] = "address.test";

  IPAddress expected;
  CHECK(expected.AssignFromIPLiteral("2001:4860:4860::8888"));

  DnsResponse response = BuildTestDnsAddressResponse(kName, expected);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::AAAA,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::AAAA, kDnsSource,
                  /*expiration_matcher=*/Ne(std::nullopt),
                  /*timed_expiration_matcher=*/Ne(std::nullopt),
                  ElementsAre(IPEndPoint(expected, /*port=*/0))))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsSingleARecordWithCname) {
  const IPAddress kExpected(192, 168, 0, 1);
  constexpr char kName[] = "address.test";
  constexpr char kCanonicalName[] = "alias.test";

  DnsResponse response =
      BuildTestDnsAddressResponseWithCname(kName, kExpected, kCanonicalName);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalDataResult(
              kCanonicalName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              ElementsAre(IPEndPoint(kExpected, /*port=*/0)))),
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), kCanonicalName))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsARecordsWithCname) {
  constexpr char kName[] = "addresses.test";

  DnsResponse response = BuildTestDnsResponse(
      "addresses.test", dns_protocol::kTypeA,
      {
          BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 179)),
          BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 180)),
          BuildTestCnameRecord(kName, "alias.test"),
          BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 176)),
          BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 177)),
          BuildTestAddressRecord("alias.test", IPAddress(74, 125, 226, 178)),
      });
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalDataResult(
              "alias.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              UnorderedElementsAre(
                  IPEndPoint(IPAddress(74, 125, 226, 179), /*port=*/0),
                  IPEndPoint(IPAddress(74, 125, 226, 180), /*port=*/0),
                  IPEndPoint(IPAddress(74, 125, 226, 176), /*port=*/0),
                  IPEndPoint(IPAddress(74, 125, 226, 177), /*port=*/0),
                  IPEndPoint(IPAddress(74, 125, 226, 178), /*port=*/0)))),
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "alias.test"))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainAResponses) {
  constexpr char kName[] = "address.test";
  constexpr auto kTtl = base::Hours(2);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeA, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)},
      /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::A, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNodataAResponses) {
  constexpr char kName[] = "address.test";
  constexpr auto kTtl = base::Minutes(15);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeA, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::A, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, RejectsMalformedARecord) {
  constexpr char kName[] = "address.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeA,
      {BuildTestDnsRecord(kName, dns_protocol::kTypeA,
                          "malformed rdata")} /* answers */);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::A,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kMalformedRecord);
}

TEST_F(DnsResponseResultExtractorTest, RejectsWrongNameARecord) {
  constexpr char kName[] = "address.test";

  DnsResponse response = BuildTestDnsAddressResponse(
      kName, IPAddress(1, 2, 3, 4), "different.test");
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::A,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kNameMismatch);
}

TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeRecordsInAResponse) {
  constexpr char kName[] = "address.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeA,
      {BuildTestTextRecord("address.test", {"foo"} /* text_strings */)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  // Expect empty results because NODATA is not cacheable (due to no TTL).
  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(), IsEmpty());
}

TEST_F(DnsResponseResultExtractorTest,
       IgnoresWrongTypeRecordsMixedWithARecords) {
  constexpr char kName[] = "address.test";
  const IPAddress kExpected(8, 8, 8, 8);
  constexpr auto kTtl = base::Days(3);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeA,
      {BuildTestTextRecord(kName, /*text_strings=*/{"foo"}, base::Hours(2)),
       BuildTestAddressRecord(kName, kExpected, kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::A, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ElementsAre(IPEndPoint(kExpected, /*port=*/0))))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsMinATtl) {
  constexpr char kName[] = "name.test";
  constexpr base::TimeDelta kMinTtl = base::Minutes(4);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeA,
      {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4), base::Hours(3)),
       BuildTestAddressRecord(kName, IPAddress(2, 3, 4, 5), kMinTtl),
       BuildTestAddressRecord(kName, IPAddress(3, 4, 5, 6),
                              base::Minutes(15))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::A, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kMinTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kMinTtl),
                  /*endpoints_matcher=*/SizeIs(3)))));
}

MATCHER_P(ContainsContiguousElements, elements, "") {
  return base::ranges::search(arg, elements) != arg.end();
}

TEST_F(DnsResponseResultExtractorTest, ExtractsTxtResponses) {
  constexpr char kName[] = "name.test";

  // Simulate two separate DNS records, each with multiple strings.
  std::vector<std::string> foo_records = {"foo1", "foo2", "foo3"};
  std::vector<std::string> bar_records = {"bar1", "bar2"};
  std::vector<std::vector<std::string>> text_records = {foo_records,
                                                        bar_records};

  DnsResponse response =
      BuildTestDnsTextResponse(kName, std::move(text_records));
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  // Order between separate DNS records is undefined, but each record should
  // stay in order as that order may be meaningful.
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
          kName, DnsQueryType::TXT, kDnsSource,
          /*expiration_matcher=*/Ne(std::nullopt),
          /*timed_expiration_matcher=*/Ne(std::nullopt),
          /*endpoints_matcher=*/IsEmpty(),
          /*strings_matcher=*/
          AllOf(UnorderedElementsAre("foo1", "foo2", "foo3", "bar1", "bar2"),
                ContainsContiguousElements(foo_records),
                ContainsContiguousElements(bar_records))))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainTxtResponses) {
  constexpr char kName[] = "name.test";
  constexpr auto kTtl = base::Days(4);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)},
      /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::TXT, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNodataTxtResponses) {
  constexpr char kName[] = "name.test";
  constexpr auto kTtl = base::Minutes(42);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT,
      /*answers=*/{}, /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::TXT, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, RejectsMalformedTxtRecord) {
  constexpr char kName[] = "name.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT,
      {BuildTestDnsRecord(kName, dns_protocol::kTypeTXT,
                          "malformed rdata")} /* answers */);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kMalformedRecord);
}

TEST_F(DnsResponseResultExtractorTest, RejectsWrongNameTxtRecord) {
  constexpr char kName[] = "name.test";

  DnsResponse response =
      BuildTestDnsTextResponse(kName, {{"foo"}}, "different.test");
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kNameMismatch);
}

TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeTxtResponses) {
  constexpr char kName[] = "name.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT,
      {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  // Expect empty results because NODATA is not cacheable (due to no TTL).
  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(), IsEmpty());
}

TEST_F(DnsResponseResultExtractorTest, ExtractsMinTxtTtl) {
  constexpr char kName[] = "name.test";
  constexpr base::TimeDelta kMinTtl = base::Minutes(4);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT,
      {BuildTestTextRecord(kName, {"foo"}, base::Hours(3)),
       BuildTestTextRecord(kName, {"bar"}, kMinTtl),
       BuildTestTextRecord(kName, {"baz"}, base::Minutes(15))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::TXT, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kMinTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kMinTtl),
                  /*endpoints_matcher=*/IsEmpty(),
                  /*strings_matcher=*/SizeIs(3)))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsPtrResponses) {
  constexpr char kName[] = "name.test";

  DnsResponse response =
      BuildTestDnsPointerResponse(kName, {"foo.com", "bar.com"});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::PTR,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::PTR, kDnsSource,
                  /*expiration_matcher=*/Ne(std::nullopt),
                  /*timed_expiration_matcher=*/Ne(std::nullopt),
                  /*endpoints_matcher=*/IsEmpty(),
                  /*strings_matcher=*/IsEmpty(),
                  /*hosts_matcher=*/
                  UnorderedElementsAre(HostPortPair("foo.com", 0),
                                       HostPortPair("bar.com", 0))))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainPtrResponses) {
  constexpr char kName[] = "name.test";
  constexpr auto kTtl = base::Hours(5);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypePTR, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)},
      /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::PTR,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::PTR, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNodataPtrResponses) {
  constexpr char kName[] = "name.test";
  constexpr auto kTtl = base::Minutes(50);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypePTR, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::PTR,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::PTR, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, RejectsMalformedPtrRecord) {
  constexpr char kName[] = "name.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypePTR,
      {BuildTestDnsRecord(kName, dns_protocol::kTypePTR,
                          "malformed rdata")} /* answers */);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::PTR,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kMalformedRecord);
}

TEST_F(DnsResponseResultExtractorTest, RejectsWrongNamePtrRecord) {
  constexpr char kName[] = "name.test";

  DnsResponse response = BuildTestDnsPointerResponse(
      kName, {"foo.com", "bar.com"}, "different.test");
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::PTR,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kNameMismatch);
}

TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypePtrResponses) {
  constexpr char kName[] = "name.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypePTR,
      {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::PTR,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  // Expect empty results because NODATA is not cacheable (due to no TTL).
  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(), IsEmpty());
}

TEST_F(DnsResponseResultExtractorTest, ExtractsSrvResponses) {
  constexpr char kName[] = "name.test";

  const TestServiceRecord kRecord1 = {2, 3, 1223, "foo.com"};
  const TestServiceRecord kRecord2 = {5, 10, 80, "bar.com"};
  const TestServiceRecord kRecord3 = {5, 1, 5, "google.com"};
  const TestServiceRecord kRecord4 = {2, 100, 12345, "chromium.org"};

  DnsResponse response = BuildTestDnsServiceResponse(
      kName, {kRecord1, kRecord2, kRecord3, kRecord4});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::SRV,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::SRV, kDnsSource,
                  /*expiration_matcher=*/Ne(std::nullopt),
                  /*timed_expiration_matcher=*/Ne(std::nullopt),
                  /*endpoints_matcher=*/IsEmpty(),
                  /*strings_matcher=*/IsEmpty(),
                  /*hosts_matcher=*/
                  UnorderedElementsAre(HostPortPair("foo.com", 1223),
                                       HostPortPair("bar.com", 80),
                                       HostPortPair("google.com", 5),
                                       HostPortPair("chromium.org", 12345))))));

  // Expect ordered by priority, and random within a priority.
  std::vector<HostPortPair> result_hosts =
      (*results.value().begin())->AsData().hosts();
  auto priority2 =
      std::vector<HostPortPair>(result_hosts.begin(), result_hosts.begin() + 2);
  EXPECT_THAT(priority2, testing::UnorderedElementsAre(
                             HostPortPair("foo.com", 1223),
                             HostPortPair("chromium.org", 12345)));
  auto priority5 =
      std::vector<HostPortPair>(result_hosts.begin() + 2, result_hosts.end());
  EXPECT_THAT(priority5,
              testing::UnorderedElementsAre(HostPortPair("bar.com", 80),
                                            HostPortPair("google.com", 5)));
}

// 0-weight services are allowed. Ensure that we can handle such records,
// especially the case where all entries have weight 0.
TEST_F(DnsResponseResultExtractorTest, ExtractsZeroWeightSrvResponses) {
  constexpr char kName[] = "name.test";

  const TestServiceRecord kRecord1 = {5, 0, 80, "bar.com"};
  const TestServiceRecord kRecord2 = {5, 0, 5, "google.com"};

  DnsResponse response =
      BuildTestDnsServiceResponse(kName, {kRecord1, kRecord2});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::SRV,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::SRV, kDnsSource,
                  /*expiration_matcher=*/Ne(std::nullopt),
                  /*timed_expiration_matcher=*/Ne(std::nullopt),
                  /*endpoints_matcher=*/IsEmpty(),
                  /*strings_matcher=*/IsEmpty(),
                  /*hosts_matcher=*/
                  UnorderedElementsAre(HostPortPair("bar.com", 80),
                                       HostPortPair("google.com", 5))))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainSrvResponses) {
  constexpr char kName[] = "name.test";
  constexpr auto kTtl = base::Days(7);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeSRV, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)},
      /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::SRV,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::SRV, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNodataSrvResponses) {
  constexpr char kName[] = "name.test";
  constexpr auto kTtl = base::Hours(12);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeSRV, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::SRV,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::SRV, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, RejectsMalformedSrvRecord) {
  constexpr char kName[] = "name.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeSRV,
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSRV,
                          "malformed rdata")} /* answers */);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::SRV,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kMalformedRecord);
}

TEST_F(DnsResponseResultExtractorTest, RejectsWrongNameSrvRecord) {
  constexpr char kName[] = "name.test";

  const TestServiceRecord kRecord = {2, 3, 1223, "foo.com"};
  DnsResponse response =
      BuildTestDnsServiceResponse(kName, {kRecord}, "different.test");
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::SRV,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kNameMismatch);
}

TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeSrvResponses) {
  constexpr char kName[] = "name.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeSRV,
      {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::SRV,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  // Expect empty results because NODATA is not cacheable (due to no TTL).
  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(), IsEmpty());
}

TEST_F(DnsResponseResultExtractorTest, ExtractsBasicHttpsResponses) {
  constexpr char kName[] = "https.test";
  constexpr auto kTtl = base::Hours(12);

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeHttps,
                           {BuildTestHttpsServiceRecord(kName,
                                                        /*priority=*/4,
                                                        /*service_name=*/".",
                                                        /*params=*/{}, kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          Eq(tick_clock_.NowTicks() + kTtl), Eq(clock_.Now() + kTtl),
          ElementsAre(
              Pair(4, ExpectConnectionEndpointMetadata(
                          ElementsAre(dns_protocol::kHttpsServiceDefaultAlpn),
                          /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsComprehensiveHttpsResponses) {
  constexpr char kName[] = "https.test";
  constexpr char kAlpn[] = "foo";
  constexpr uint8_t kEchConfig[] = "EEEEEEEEECH!";
  constexpr auto kTtl = base::Hours(12);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(
           kName, /*priority=*/4,
           /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({kAlpn}),
            BuildTestHttpsServiceEchConfigParam(kEchConfig)},
           kTtl),
       BuildTestHttpsServiceRecord(
           kName, /*priority=*/3,
           /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({kAlpn}),
            {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}},
           /*ttl=*/base::Days(3))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          Eq(tick_clock_.NowTicks() + kTtl), Eq(clock_.Now() + kTtl),
          ElementsAre(
              Pair(3, ExpectConnectionEndpointMetadata(
                          ElementsAre(kAlpn),
                          /*ech_config_list_matcher=*/IsEmpty(), kName)),
              Pair(4, ExpectConnectionEndpointMetadata(
                          ElementsAre(kAlpn,
                                      dns_protocol::kHttpsServiceDefaultAlpn),
                          ElementsAreArray(kEchConfig), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsResponseWithJustAlias) {
  constexpr char kName[] = "https.test";
  constexpr base::TimeDelta kTtl = base::Days(5);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsAliasRecord(kName, "alias.test", kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  // Expect empty metadata result to signify compatible HTTPS records with no
  // data of use to Chrome. Still expect expiration from record, so the empty
  // response can be cached.
  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl),
          /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl),
          /*metadatas_matcher=*/IsEmpty()))));
}

TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsResponseWithAlias) {
  constexpr char kName[] = "https.test";
  constexpr base::TimeDelta kLowestTtl = base::Minutes(32);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(kName,
                                   /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/{}, base::Days(1)),
       BuildTestHttpsAliasRecord(kName, "alias.test", kLowestTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  // Expect empty metadata result to signify compatible HTTPS records with no
  // data of use to Chrome. Expiration should match lowest TTL from all
  // compatible records.
  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kLowestTtl),
          /*timed_expiration_matcher=*/Optional(clock_.Now() + kLowestTtl),
          /*metadatas_matcher=*/IsEmpty()))));
}

// Expect the entire response to be ignored if all HTTPS records have the
// "no-default-alpn" param.
TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsResponseWithNoDefaultAlpn) {
  constexpr char kName[] = "https.test";
  constexpr base::TimeDelta kLowestTtl = base::Hours(3);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(
           kName, /*priority=*/4,
           /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({"foo1"}),
            {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}},
           kLowestTtl),
       BuildTestHttpsServiceRecord(
           kName, /*priority=*/5,
           /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({"foo2"}),
            {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}},
           base::Days(3))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  // Expect empty metadata result to signify compatible HTTPS records with no
  // data of use to Chrome.
  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kLowestTtl),
          /*timed_expiration_matcher=*/Optional(clock_.Now() + kLowestTtl),
          /*metadatas_matcher=*/IsEmpty()))));
}

// Unsupported/unknown HTTPS params are simply ignored if not marked mandatory.
TEST_F(DnsResponseResultExtractorTest, IgnoresUnsupportedParamsInHttpsRecord) {
  constexpr char kName[] = "https.test";
  constexpr uint16_t kMadeUpParamKey = 65500;  // From the private-use block.

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(kName, /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {{kMadeUpParamKey, "foo"}})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Ne(std::nullopt),
          /*timed_expiration_matcher=*/Ne(std::nullopt),
          ElementsAre(
              Pair(4, ExpectConnectionEndpointMetadata(
                          ElementsAre(dns_protocol::kHttpsServiceDefaultAlpn),
                          /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

// Entire record is dropped if an unsupported/unknown HTTPS param is marked
// mandatory.
TEST_F(DnsResponseResultExtractorTest,
       IgnoresHttpsRecordWithUnsupportedMandatoryParam) {
  constexpr char kName[] = "https.test";
  constexpr uint16_t kMadeUpParamKey = 65500;  // From the private-use block.
  constexpr base::TimeDelta kTtl = base::Days(5);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(
           kName, /*priority=*/4,
           /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({"ignored_alpn"}),
            BuildTestHttpsServiceMandatoryParam({kMadeUpParamKey}),
            {kMadeUpParamKey, "foo"}},
           base::Hours(2)),
       BuildTestHttpsServiceRecord(
           kName, /*priority=*/5,
           /*service_name=*/".",
           /*params=*/{BuildTestHttpsServiceAlpnParam({"foo"})}, kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());

  // Expect expiration to be derived only from non-ignored records.
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl),
          /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl),
          ElementsAre(Pair(
              5, ExpectConnectionEndpointMetadata(
                     ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                     /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest,
       ExtractsHttpsRecordWithMatchingServiceName) {
  constexpr char kName[] = "https.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(kName, /*priority=*/4,
                                   /*service_name=*/kName,
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Ne(std::nullopt),
          /*timed_expiration_matcher=*/Ne(std::nullopt),
          ElementsAre(Pair(
              4, ExpectConnectionEndpointMetadata(
                     ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                     /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest,
       ExtractsHttpsRecordWithMatchingDefaultServiceName) {
  constexpr char kName[] = "https.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(kName, /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Ne(std::nullopt),
          /*timed_expiration_matcher=*/Ne(std::nullopt),
          ElementsAre(Pair(
              4, ExpectConnectionEndpointMetadata(
                     ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                     /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest,
       ExtractsHttpsRecordWithPrefixedNameAndMatchingServiceName) {
  constexpr char kName[] = "https.test";
  constexpr char kPrefixedName[] = "_444._https.https.test";

  DnsResponse response = BuildTestDnsResponse(
      kPrefixedName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(kPrefixedName, /*priority=*/4,
                                   /*service_name=*/kName,
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kPrefixedName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Ne(std::nullopt),
          /*timed_expiration_matcher=*/Ne(std::nullopt),
          ElementsAre(Pair(
              4, ExpectConnectionEndpointMetadata(
                     ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                     /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest,
       ExtractsHttpsRecordWithAliasingAndMatchingServiceName) {
  constexpr char kName[] = "https.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestCnameRecord(kName, "alias.test"),
       BuildTestHttpsServiceRecord("alias.test", /*priority=*/4,
                                   /*service_name=*/kName,
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::HTTPS, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "alias.test")),
          Pointee(ExpectHostResolverInternalMetadataResult(
              "alias.test", DnsQueryType::HTTPS, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              ElementsAre(Pair(
                  4, ExpectConnectionEndpointMetadata(
                         ElementsAre("foo",
                                     dns_protocol::kHttpsServiceDefaultAlpn),
                         /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest,
       IgnoreHttpsRecordWithNonMatchingServiceName) {
  constexpr char kName[] = "https.test";
  constexpr base::TimeDelta kTtl = base::Hours(14);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(
           kName, /*priority=*/4,
           /*service_name=*/"other.service.test",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({"ignored"})}, base::Hours(3)),
       BuildTestHttpsServiceRecord("https.test", /*priority=*/5,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})},
                                   kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());

  // Expect expiration to be derived only from non-ignored records.
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl),
          /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl),
          ElementsAre(Pair(
              5, ExpectConnectionEndpointMetadata(
                     ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                     /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest,
       ExtractsHttpsRecordWithPrefixedNameAndDefaultServiceName) {
  constexpr char kPrefixedName[] = "_445._https.https.test";

  DnsResponse response = BuildTestDnsResponse(
      kPrefixedName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(kPrefixedName, /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/"https.test",
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kPrefixedName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Ne(std::nullopt),
          /*timed_expiration_matcher=*/Ne(std::nullopt),
          ElementsAre(Pair(
              4,
              ExpectConnectionEndpointMetadata(
                  ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                  /*ech_config_list_matcher=*/IsEmpty(), kPrefixedName)))))));
}

TEST_F(DnsResponseResultExtractorTest,
       ExtractsHttpsRecordWithAliasingAndDefaultServiceName) {
  constexpr char kName[] = "https.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestCnameRecord(kName, "alias.test"),
       BuildTestHttpsServiceRecord("alias.test", /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::HTTPS, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "alias.test")),
          Pointee(ExpectHostResolverInternalMetadataResult(
              "alias.test", DnsQueryType::HTTPS, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              ElementsAre(Pair(
                  4, ExpectConnectionEndpointMetadata(
                         ElementsAre("foo",
                                     dns_protocol::kHttpsServiceDefaultAlpn),
                         /*ech_config_list_matcher=*/IsEmpty(),
                         "alias.test")))))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsHttpsRecordWithMatchingPort) {
  constexpr char kName[] = "https.test";
  constexpr uint16_t kPort = 4567;

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(kName, /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"}),
                                    BuildTestHttpsServicePortParam(kPort)})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/kPort);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Ne(std::nullopt),
          /*timed_expiration_matcher=*/Ne(std::nullopt),
          ElementsAre(Pair(
              4, ExpectConnectionEndpointMetadata(
                     ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                     /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsRecordWithMismatchingPort) {
  constexpr char kName[] = "https.test";
  constexpr base::TimeDelta kTtl = base::Days(14);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(kName, /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"ignored"}),
                                    BuildTestHttpsServicePortParam(1003)},
                                   base::Hours(12)),
       BuildTestHttpsServiceRecord(kName, /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})},
                                   kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/55);

  ASSERT_TRUE(results.has_value());

  // Expect expiration to be derived only from non-ignored records.
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl),
          /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl),
          ElementsAre(Pair(
              4, ExpectConnectionEndpointMetadata(
                     ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                     /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

// HTTPS records with "no-default-alpn" but also no "alpn" are not
// "self-consistent" and should be ignored.
TEST_F(DnsResponseResultExtractorTest, IgnoresHttpsRecordWithNoAlpn) {
  constexpr char kName[] = "https.test";
  constexpr base::TimeDelta kTtl = base::Minutes(150);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(
           kName, /*priority=*/4,
           /*service_name=*/".",
           /*params=*/
           {{dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}},
           base::Minutes(10)),
       BuildTestHttpsServiceRecord(kName, /*priority=*/4,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo"})},
                                   kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/55);

  ASSERT_TRUE(results.has_value());

  // Expect expiration to be derived only from non-ignored records.
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kTtl),
          /*timed_expiration_matcher=*/Optional(clock_.Now() + kTtl),
          ElementsAre(Pair(
              4, ExpectConnectionEndpointMetadata(
                     ElementsAre("foo", dns_protocol::kHttpsServiceDefaultAlpn),
                     /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

// Expect the entire response to be ignored if all HTTPS records have the
// "no-default-alpn" param.
TEST_F(DnsResponseResultExtractorTest,
       IgnoresHttpsResponseWithNoCompatibleDefaultAlpn) {
  constexpr char kName[] = "https.test";
  constexpr uint16_t kMadeUpParamKey = 65500;  // From the private-use block.
  constexpr base::TimeDelta kLowestTtl = base::Days(2);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsServiceRecord(
           kName, /*priority=*/4,
           /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({"foo1"}),
            {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}},
           base::Days(3)),
       BuildTestHttpsServiceRecord(
           kName, /*priority=*/5,
           /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({"foo2"}),
            {dns_protocol::kHttpsServiceParamKeyNoDefaultAlpn, ""}},
           base::Days(4)),
       // Allows default ALPN, but ignored due to non-matching service name.
       BuildTestHttpsServiceRecord(kName, /*priority=*/3,
                                   /*service_name=*/"other.test",
                                   /*params=*/{}, kLowestTtl),
       // Allows default ALPN, but ignored due to incompatible param.
       BuildTestHttpsServiceRecord(
           kName, /*priority=*/6,
           /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceMandatoryParam({kMadeUpParamKey}),
            {kMadeUpParamKey, "foo"}},
           base::Hours(1)),
       // Allows default ALPN, but ignored due to mismatching port.
       BuildTestHttpsServiceRecord(
           kName, /*priority=*/10,
           /*service_name=*/".",
           /*params=*/{BuildTestHttpsServicePortParam(1005)}, base::Days(5))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());

  // Expect expiration to be from the lowest TTL from the "compatible" records
  // that don't have incompatible params.
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          /*expiration_matcher=*/Optional(tick_clock_.NowTicks() + kLowestTtl),
          /*timed_expiration_matcher=*/Optional(clock_.Now() + kLowestTtl),
          /*metadatas_matcher=*/IsEmpty()))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNxdomainHttpsResponses) {
  constexpr char kName[] = "https.test";
  constexpr auto kTtl = base::Minutes(45);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)},
      /*additional=*/{}, dns_protocol::kRcodeNXDOMAIN);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::HTTPS, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsNodataHttpsResponses) {
  constexpr char kName[] = "https.test";
  constexpr auto kTtl = base::Hours(36);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps, /*answers=*/{},
      /*authority=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeSOA, "fake rdata", kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalErrorResult(
                  kName, DnsQueryType::HTTPS, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  ERR_NAME_NOT_RESOLVED))));
}

TEST_F(DnsResponseResultExtractorTest, RejectsMalformedHttpsRecord) {
  constexpr char kName[] = "https.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestDnsRecord(kName, dns_protocol::kTypeHttps,
                          "malformed rdata")} /* answers */);
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::HTTPS,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kMalformedRecord);
}

TEST_F(DnsResponseResultExtractorTest, RejectsWrongNameHttpsRecord) {
  constexpr char kName[] = "https.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestHttpsAliasRecord("different.test", "alias.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::HTTPS,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kNameMismatch);
}

TEST_F(DnsResponseResultExtractorTest, IgnoresWrongTypeHttpsResponses) {
  constexpr char kName[] = "https.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      {BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(results.value(), IsEmpty());
}

TEST_F(DnsResponseResultExtractorTest, IgnoresAdditionalHttpsRecords) {
  constexpr char kName[] = "https.test";
  constexpr auto kTtl = base::Days(5);

  // Give all records an "alpn" value to help validate that only the correct
  // record is used.
  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeHttps,
      /*answers=*/
      {BuildTestHttpsServiceRecord(kName, /*priority=*/5u,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo1"})},
                                   kTtl)},
      /*authority=*/{},
      /*additional=*/
      {BuildTestHttpsServiceRecord(kName, /*priority=*/3u,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo2"})},
                                   base::Minutes(44)),
       BuildTestHttpsServiceRecord(kName, /*priority=*/2u,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo3"})},
                                   base::Minutes(30))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::HTTPS,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(Pointee(ExpectHostResolverInternalMetadataResult(
          kName, DnsQueryType::HTTPS, kDnsSource,
          Eq(tick_clock_.NowTicks() + kTtl), Eq(clock_.Now() + kTtl),
          ElementsAre(Pair(
              5,
              ExpectConnectionEndpointMetadata(
                  ElementsAre("foo1", dns_protocol::kHttpsServiceDefaultAlpn),
                  /*ech_config_list_matcher=*/IsEmpty(), kName)))))));
}

TEST_F(DnsResponseResultExtractorTest, IgnoresUnsolicitedHttpsRecords) {
  constexpr char kName[] = "name.test";
  constexpr auto kTtl = base::Minutes(45);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT,
      /*answers=*/
      {BuildTestDnsRecord(kName, dns_protocol::kTypeTXT, "\003foo", kTtl)},
      /*authority=*/{},
      /*additional=*/
      {BuildTestHttpsServiceRecord(
           "https.test", /*priority=*/3u, /*service_name=*/".",
           /*params=*/
           {BuildTestHttpsServiceAlpnParam({"foo2"})}, base::Minutes(44)),
       BuildTestHttpsServiceRecord("https.test", /*priority=*/2u,
                                   /*service_name=*/".",
                                   /*params=*/
                                   {BuildTestHttpsServiceAlpnParam({"foo3"})},
                                   base::Minutes(30))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());

  // Expect expiration to be derived only from the non-ignored answer record.
  EXPECT_THAT(results.value(),
              ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
                  kName, DnsQueryType::TXT, kDnsSource,
                  /*expiration_matcher=*/Eq(tick_clock_.NowTicks() + kTtl),
                  /*timed_expiration_matcher=*/Eq(clock_.Now() + kTtl),
                  /*endpoints_matcher=*/IsEmpty(), ElementsAre("foo")))));
}

TEST_F(DnsResponseResultExtractorTest, HandlesInOrderCnameChain) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord(kName, "second.test"),
                            BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestTextRecord("fourth.test", {"foo"}),
                            BuildTestTextRecord("fourth.test", {"bar"})});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "fourth.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              /*endpoints_matcher=*/IsEmpty(),
              UnorderedElementsAre("foo", "bar")))));
}

TEST_F(DnsResponseResultExtractorTest, HandlesInOrderCnameChainTypeA) {
  constexpr char kName[] = "first.test";

  const IPAddress kExpected(192, 168, 0, 1);
  IPEndPoint expected_endpoint(kExpected, 0 /* port */);

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeA,
                           {BuildTestCnameRecord(kName, "second.test"),
                            BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestAddressRecord("fourth.test", kExpected)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "fourth.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              ElementsAre(expected_endpoint)))));
}

TEST_F(DnsResponseResultExtractorTest, HandlesReverseOrderCnameChain) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestTextRecord("fourth.test", {"foo"}),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "fourth.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              /*endpoints_matcher=*/IsEmpty(), ElementsAre("foo")))));
}

TEST_F(DnsResponseResultExtractorTest, HandlesReverseOrderCnameChainTypeA) {
  constexpr char kName[] = "first.test";

  const IPAddress kExpected(192, 168, 0, 1);
  IPEndPoint expected_endpoint(kExpected, 0 /* port */);

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeA,
                           {BuildTestAddressRecord("fourth.test", kExpected),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "fourth.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              ElementsAre(expected_endpoint)))));
}

TEST_F(DnsResponseResultExtractorTest, HandlesArbitraryOrderCnameChain) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestTextRecord("fourth.test", {"foo"}),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "fourth.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              /*endpoints_matcher=*/IsEmpty(), ElementsAre("foo")))));
}

TEST_F(DnsResponseResultExtractorTest, HandlesArbitraryOrderCnameChainTypeA) {
  constexpr char kName[] = "first.test";

  const IPAddress kExpected(192, 168, 0, 1);
  IPEndPoint expected_endpoint(kExpected, 0 /* port */);

  // Alias names are chosen so that the chain order is not in alphabetical
  // order.
  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeA,
                           {BuildTestCnameRecord("qsecond.test", "athird.test"),
                            BuildTestAddressRecord("zfourth.test", kExpected),
                            BuildTestCnameRecord("athird.test", "zfourth.test"),
                            BuildTestCnameRecord(kName, "qsecond.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "qsecond.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "qsecond.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "athird.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "athird.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "zfourth.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "zfourth.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              ElementsAre(expected_endpoint)))));
}

TEST_F(DnsResponseResultExtractorTest,
       IgnoresNonResultTypesMixedWithCnameChain) {
  constexpr char kName[] = "first.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT,
      {BuildTestCnameRecord("second.test", "third.test"),
       BuildTestTextRecord("fourth.test", {"foo"}),
       BuildTestCnameRecord("third.test", "fourth.test"),
       BuildTestAddressRecord("third.test", IPAddress(1, 2, 3, 4)),
       BuildTestCnameRecord(kName, "second.test"),
       BuildTestAddressRecord("fourth.test", IPAddress(2, 3, 4, 5))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "fourth.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              /*endpoints_matcher=*/IsEmpty(), ElementsAre("foo")))));
}

TEST_F(DnsResponseResultExtractorTest,
       IgnoresNonResultTypesMixedWithCnameChainTypeA) {
  constexpr char kName[] = "first.test";

  const IPAddress kExpected(192, 168, 0, 1);
  IPEndPoint expected_endpoint(kExpected, 0 /* port */);

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeA,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestTextRecord("fourth.test", {"foo"}),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord(kName, "second.test"),
                            BuildTestAddressRecord("fourth.test", kExpected)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "fourth.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              ElementsAre(expected_endpoint)))));
}

TEST_F(DnsResponseResultExtractorTest, HandlesCnameChainWithoutResult) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::TXT, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test"))));
}

TEST_F(DnsResponseResultExtractorTest, HandlesCnameChainWithoutResultTypeA) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeA,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "second.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "second.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "third.test")),
          Pointee(ExpectHostResolverInternalAliasResult(
              "third.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "fourth.test"))));
}

TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithLoop) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestTextRecord("third.test", {"foo"}),
                            BuildTestCnameRecord("third.test", "second.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kBadAliasChain);
}

TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithLoopToBeginning) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestTextRecord("third.test", {"foo"}),
                            BuildTestCnameRecord("third.test", "first.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kBadAliasChain);
}

TEST_F(DnsResponseResultExtractorTest,
       RejectsCnameChainWithLoopToBeginningWithoutResult) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestCnameRecord("third.test", "first.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kBadAliasChain);
}

TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithWrongStart) {
  constexpr char kName[] = "test.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestTextRecord("fourth.test", {"foo"}),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord("first.test", "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kBadAliasChain);
}

TEST_F(DnsResponseResultExtractorTest, RejectsCnameChainWithWrongResultName) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestTextRecord("third.test", {"foo"}),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kNameMismatch);
}

TEST_F(DnsResponseResultExtractorTest, RejectsCnameSharedWithResult) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestTextRecord(kName, {"foo"}),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kNameMismatch);
}

TEST_F(DnsResponseResultExtractorTest, RejectsDisjointCnameChain) {
  constexpr char kName[] = "first.test";

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT,
      {BuildTestCnameRecord("second.test", "third.test"),
       BuildTestTextRecord("fourth.test", {"foo"}),
       BuildTestCnameRecord("third.test", "fourth.test"),
       BuildTestCnameRecord("other1.test", "other2.test"),
       BuildTestCnameRecord(kName, "second.test"),
       BuildTestCnameRecord("other2.test", "other3.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kBadAliasChain);
}

TEST_F(DnsResponseResultExtractorTest, RejectsDoubledCnames) {
  constexpr char kName[] = "first.test";

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord("second.test", "third.test"),
                            BuildTestTextRecord("fourth.test", {"foo"}),
                            BuildTestCnameRecord("third.test", "fourth.test"),
                            BuildTestCnameRecord("third.test", "fifth.test"),
                            BuildTestCnameRecord(kName, "second.test")});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::TXT,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kMultipleCnames);
}

TEST_F(DnsResponseResultExtractorTest, IgnoresTtlFromNonResultType) {
  constexpr char kName[] = "name.test";
  constexpr base::TimeDelta kMinTtl = base::Minutes(4);

  DnsResponse response = BuildTestDnsResponse(
      kName, dns_protocol::kTypeTXT,
      {BuildTestTextRecord(kName, {"foo"}, base::Hours(3)),
       BuildTestTextRecord(kName, {"bar"}, kMinTtl),
       BuildTestAddressRecord(kName, IPAddress(1, 2, 3, 4), base::Seconds(2)),
       BuildTestTextRecord(kName, {"baz"}, base::Minutes(15))});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      ElementsAre(Pointee(ExpectHostResolverInternalDataResult(
          kName, DnsQueryType::TXT, kDnsSource,
          Eq(tick_clock_.NowTicks() + kMinTtl), Eq(clock_.Now() + kMinTtl),
          /*endpoints_matcher=*/IsEmpty(),
          UnorderedElementsAre("foo", "bar", "baz")))));
}

TEST_F(DnsResponseResultExtractorTest, ExtractsTtlFromCname) {
  constexpr char kName[] = "name.test";
  constexpr char kAlias[] = "alias.test";
  constexpr base::TimeDelta kTtl = base::Minutes(4);

  DnsResponse response =
      BuildTestDnsResponse("name.test", dns_protocol::kTypeTXT,
                           {BuildTestCnameRecord(kName, kAlias, kTtl)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::TXT,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(Pointee(ExpectHostResolverInternalAliasResult(
          kName, DnsQueryType::TXT, kDnsSource,
          Eq(tick_clock_.NowTicks() + kTtl), Eq(clock_.Now() + kTtl),
          kAlias))));
}

TEST_F(DnsResponseResultExtractorTest, ValidatesAliasNames) {
  constexpr char kName[] = "first.test";

  const IPAddress kExpected(192, 168, 0, 1);
  IPEndPoint expected_endpoint(kExpected, 0 /* port */);

  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeA,
                           {BuildTestCnameRecord(kName, "second.test"),
                            BuildTestCnameRecord("second.test", "localhost"),
                            BuildTestCnameRecord("localhost", "fourth.test"),
                            BuildTestAddressRecord("fourth.test", kExpected)});
  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  EXPECT_EQ(extractor
                .ExtractDnsResults(DnsQueryType::A,
                                   /*original_domain_name=*/kName,
                                   /*request_port=*/0)
                .error_or(ExtractionError::kOk),
            ExtractionError::kMalformedRecord);
}

TEST_F(DnsResponseResultExtractorTest, CanonicalizesAliasNames) {
  const IPAddress kExpected(192, 168, 0, 1);
  constexpr char kName[] = "address.test";
  constexpr char kCname[] = "\005ALIAS\004test\000";

  // Need to build records directly in order to manually encode alias target
  // name because BuildTestDnsAddressResponseWithCname() uses
  // DNSDomainFromDot() which does not support non-URL-canonicalized names.
  std::vector<DnsResourceRecord> answers = {
      BuildTestDnsRecord(kName, dns_protocol::kTypeCNAME,
                         std::string(kCname, sizeof(kCname) - 1)),
      BuildTestAddressRecord("alias.test", kExpected)};
  DnsResponse response =
      BuildTestDnsResponse(kName, dns_protocol::kTypeA, answers);

  DnsResponseResultExtractor extractor(response, clock_, tick_clock_);

  ResultsOrError results =
      extractor.ExtractDnsResults(DnsQueryType::A,
                                  /*original_domain_name=*/kName,
                                  /*request_port=*/0);

  ASSERT_TRUE(results.has_value());
  EXPECT_THAT(
      results.value(),
      UnorderedElementsAre(
          Pointee(ExpectHostResolverInternalAliasResult(
              kName, DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt), "alias.test")),
          Pointee(ExpectHostResolverInternalDataResult(
              "alias.test", DnsQueryType::A, kDnsSource,
              /*expiration_matcher=*/Ne(std::nullopt),
              /*timed_expiration_matcher=*/Ne(std::nullopt),
              ElementsAre(IPEndPoint(kExpected, /*port=*/0))))));
}

}  // namespace
}  // namespace net
