diff options
Diffstat (limited to 'chromium/net/dns/httpssvc_metrics_unittest.cc')
-rw-r--r-- | chromium/net/dns/httpssvc_metrics_unittest.cc | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/chromium/net/dns/httpssvc_metrics_unittest.cc b/chromium/net/dns/httpssvc_metrics_unittest.cc new file mode 100644 index 00000000000..1ce51cf7608 --- /dev/null +++ b/chromium/net/dns/httpssvc_metrics_unittest.cc @@ -0,0 +1,554 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/dns/httpssvc_metrics.h" + +#include <string> +#include <tuple> + +#include "base/feature_list.h" +#include "base/strings/strcat.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "net/base/features.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +// int: number of domains +// bool: extra leading comma +// bool: extra trailing comma +using DomainListQuirksTuple = std::tuple<int, bool, bool>; + +// bool: DnsHttpssvc feature is enabled +// bool: DnsHttpssvcUseIntegrity feature param +// bool: DnsHttpssvcUseHttpssvc feature param +// bool: DnsHttpssvcControlDomainWildcard feature param +using HttpssvcFeatureTuple = std::tuple<bool, bool, bool, bool>; + +// DomainListQuirksTuple: quirks for the experimental domain list. +// DomainListQuirksTuple: quirks for the control domain list. +// HttpssvcFeatureTuple: config for the whole DnsHttpssvc feature. +using ParsingTestParamTuple = std:: + tuple<DomainListQuirksTuple, DomainListQuirksTuple, HttpssvcFeatureTuple>; + +// bool: whether we are querying for an experimental domain or a control domain +// HttpssvcFeatureTuple: config for the whole DnsHttpssvc feature. +using MetricsTestParamTuple = std::tuple<bool, HttpssvcFeatureTuple>; + +// Create a comma-separated list of |domains| with the given |quirks|. +std::string FlattenDomainList(const std::vector<std::string>& domains, + DomainListQuirksTuple quirks) { + int num_domains; + bool leading_comma, trailing_comma; + std::tie(num_domains, leading_comma, trailing_comma) = quirks; + + CHECK_EQ(static_cast<size_t>(num_domains), domains.size()); + std::string flattened = base::JoinString(domains, ","); + if (leading_comma) + flattened.insert(flattened.begin(), ','); + if (trailing_comma) + flattened.push_back(','); + return flattened; +} + +// Intermediate representation constructed from test parameters. +struct HttpssvcFeatureConfig { + HttpssvcFeatureConfig() = default; + + explicit HttpssvcFeatureConfig(const HttpssvcFeatureTuple& feature_tuple, + base::StringPiece experiment_domains, + base::StringPiece control_domains) + : experiment_domains(experiment_domains.as_string()), + control_domains(control_domains.as_string()) { + std::tie(enabled, use_integrity, use_httpssvc, control_domain_wildcard) = + feature_tuple; + } + + void Apply(base::test::ScopedFeatureList* scoped_feature_list) const { + if (!enabled) { + scoped_feature_list->InitAndDisableFeature(features::kDnsHttpssvc); + return; + } + auto stringify = [](bool b) -> std::string { return b ? "true" : "false"; }; + scoped_feature_list->InitAndEnableFeatureWithParameters( + features::kDnsHttpssvc, + { + {"DnsHttpssvcUseHttpssvc", stringify(use_httpssvc)}, + {"DnsHttpssvcUseIntegrity", stringify(use_integrity)}, + {"DnsHttpssvcEnableQueryOverInsecure", "false"}, + {"DnsHttpssvcExperimentDomains", experiment_domains}, + {"DnsHttpssvcControlDomains", control_domains}, + {"DnsHttpssvcControlDomainWildcard", + stringify(control_domain_wildcard)}, + }); + } + + bool enabled = false; + bool use_integrity = false; + bool use_httpssvc = false; + bool control_domain_wildcard = false; + std::string experiment_domains; + std::string control_domains; +}; + +std::vector<std::string> GenerateDomainList(base::StringPiece label, int n) { + std::vector<std::string> domains; + for (int i = 0; i < n; i++) { + domains.push_back(base::StrCat( + {"domain", base::NumberToString(i), ".", label, ".example"})); + } + return domains; +} + +// Base for testing domain list parsing functions in +// net::features::dns_httpssvc_experiment. +class HttpssvcDomainParsingTest + : public ::testing::TestWithParam<ParsingTestParamTuple> { + public: + void SetUp() override { + DomainListQuirksTuple domain_quirks_experimental; + DomainListQuirksTuple domain_quirks_control; + HttpssvcFeatureTuple httpssvc_feature; + std::tie(domain_quirks_experimental, domain_quirks_control, + httpssvc_feature) = GetParam(); + + expected_experiment_domains_ = GenerateDomainList( + "experiment", std::get<0>(domain_quirks_experimental)); + expected_control_domains_ = + GenerateDomainList("control", std::get<0>(domain_quirks_control)); + + config_ = HttpssvcFeatureConfig( + httpssvc_feature, + FlattenDomainList(expected_experiment_domains_, + domain_quirks_experimental), + FlattenDomainList(expected_control_domains_, domain_quirks_control)); + config_.Apply(&scoped_feature_list_); + } + + const HttpssvcFeatureConfig& config() { return config_; } + + protected: + // The expected results of parsing the comma-separated domain lists in + // |experiment_domains| and |control_domains|, respectively. + std::vector<std::string> expected_experiment_domains_; + std::vector<std::string> expected_control_domains_; + + private: + HttpssvcFeatureConfig config_; + base::test::ScopedFeatureList scoped_feature_list_; +}; + +// This instantiation tests the domain list parser against various quirks, +// e.g. leading comma. +INSTANTIATE_TEST_SUITE_P( + HttpssvcMetricsTestDomainParsing, + HttpssvcDomainParsingTest, + testing::Combine( + // DomainListQuirksTuple for experimental domains. To fight back + // combinatorial explosion of tests, this tuple is pared down more than + // the one for control domains. This should not significantly hurt test + // coverage because |IsExperimentDomain| and |IsControlDomain| rely on a + // shared helper function. + testing::Combine(testing::Values(0, 1), + testing::Values(false), + testing::Values(false)), + // DomainListQuirksTuple for control domains. + testing::Combine(testing::Range(0, 3), + testing::Bool(), + testing::Bool()), + // HttpssvcFeatureTuple + testing::Combine( + testing::Bool() /* DnsHttpssvc feature enabled? */, + testing::Bool() /* DnsHttpssvcUseIntegrity */, + testing::Values(false) /* DnsHttpssvcUseHttpssvc */, + testing::Values(false) /* DnsHttpssvcControlDomainWildcard */))); + +// Base for testing the metrics collection code in |HttpssvcMetrics|. +class HttpssvcMetricsTest + : public ::testing::TestWithParam<MetricsTestParamTuple> { + public: + void SetUp() override { + HttpssvcFeatureTuple httpssvc_feature; + std::tie(querying_experimental_, httpssvc_feature) = GetParam(); + config_ = HttpssvcFeatureConfig(httpssvc_feature, "", ""); + config_.Apply(&scoped_feature_list_); + } + + std::string BuildMetricNamePrefix() const { + return base::StrCat( + {"Net.DNS.HTTPSSVC.RecordIntegrity.", doh_provider_, "."}); + } + + template <typename T> + void ExpectSample(base::StringPiece name, base::Optional<T> sample) const { + if (sample) + histo().ExpectUniqueSample(name, *sample, 1); + else + histo().ExpectTotalCount(name, 0); + } + + void ExpectSample(base::StringPiece name, + base::Optional<base::TimeDelta> sample) const { + base::Optional<int64_t> sample_ms; + if (sample) + sample_ms = {sample->InMilliseconds()}; + ExpectSample<int64_t>(name, sample_ms); + } + + void VerifyMetricsForExpectIntact( + base::Optional<HttpssvcDnsRcode> rcode, + base::Optional<bool> integrity, + base::Optional<bool> record_with_error, + base::Optional<base::TimeDelta> resolve_time_integrity, + base::Optional<base::TimeDelta> resolve_time_non_integrity, + base::Optional<int> resolve_time_ratio) const { + const std::string kPrefix = + base::StrCat({BuildMetricNamePrefix(), "ExpectIntact."}); + const std::string kMetricDnsRcode = base::StrCat({kPrefix, "DnsRcode"}); + const std::string kMetricIntegrity = base::StrCat({kPrefix, "Integrity"}); + const std::string kMetricRecordWithError = + base::StrCat({kPrefix, "RecordWithError"}); + const std::string kMetricResolveTimeIntegrity = + base::StrCat({kPrefix, "ResolveTimeIntegrityRecord"}); + const std::string kMetricResolveTimeNonIntegrity = + base::StrCat({kPrefix, "ResolveTimeNonIntegrityRecord"}); + const std::string kMetricResolveTimeRatio = + base::StrCat({kPrefix, "ResolveTimeRatio"}); + + ExpectSample(kMetricDnsRcode, rcode); + ExpectSample(kMetricIntegrity, integrity); + ExpectSample(kMetricRecordWithError, record_with_error); + ExpectSample(kMetricResolveTimeIntegrity, resolve_time_integrity); + ExpectSample(kMetricResolveTimeNonIntegrity, resolve_time_non_integrity); + ExpectSample(kMetricResolveTimeRatio, resolve_time_ratio); + } + + void VerifyMetricsForExpectNoerror( + base::Optional<HttpssvcDnsRcode> rcode, + base::Optional<int> record_received, + base::Optional<base::TimeDelta> resolve_time_integrity, + base::Optional<base::TimeDelta> resolve_time_non_integrity, + base::Optional<int> resolve_time_ratio) const { + const std::string kPrefix = + base::StrCat({BuildMetricNamePrefix(), "ExpectNoerror."}); + const std::string kMetricDnsRcode = base::StrCat({kPrefix, "DnsRcode"}); + const std::string kMetricRecordReceived = + base::StrCat({kPrefix, "RecordReceived"}); + const std::string kMetricResolveTimeIntegrity = + base::StrCat({kPrefix, "ResolveTimeIntegrityRecord"}); + const std::string kMetricResolveTimeNonIntegrity = + base::StrCat({kPrefix, "ResolveTimeNonIntegrityRecord"}); + const std::string kMetricResolveTimeRatio = + base::StrCat({kPrefix, "ResolveTimeRatio"}); + + ExpectSample(kMetricDnsRcode, rcode); + ExpectSample(kMetricRecordReceived, record_received); + ExpectSample(kMetricResolveTimeIntegrity, resolve_time_integrity); + ExpectSample(kMetricResolveTimeNonIntegrity, resolve_time_non_integrity); + ExpectSample(kMetricResolveTimeRatio, resolve_time_ratio); + } + + void VerifyMetricsForExpectIntact() { + VerifyMetricsForExpectIntact(base::nullopt, base::nullopt, base::nullopt, + base::nullopt, base::nullopt, base::nullopt); + } + + void VerifyMetricsForExpectNoerror() { + VerifyMetricsForExpectNoerror(base::nullopt, base::nullopt, base::nullopt, + base::nullopt, base::nullopt); + } + + const base::HistogramTester& histo() const { return histogram_; } + const HttpssvcFeatureConfig& config() const { return config_; } + + protected: + bool querying_experimental_; + + private: + HttpssvcFeatureConfig config_; + base::test::ScopedFeatureList scoped_feature_list_; + base::HistogramTester histogram_; + std::string doh_provider_ = "Other"; +}; + +// This instantiation focuses on whether the correct metrics are recorded. The +// domain list parser is already tested against encoding quirks in +// |HttpssvcMetricsTestDomainParsing|, so we fix the quirks at false. +INSTANTIATE_TEST_SUITE_P( + HttpssvcMetricsTestSimple, + HttpssvcMetricsTest, + testing::Combine( + // Whether we are querying an experimental domain. + testing::Bool(), + // HttpssvcFeatureTuple + testing::Combine( + testing::Values(true) /* DnsHttpssvc feature enabled? */, + testing::Values(true) /* DnsHttpssvcUseIntegrity */, + testing::Values(false) /* DnsHttpssvcUseHttpssvc */, + testing::Values(false) /* DnsHttpssvcControlDomainWildcard */))); + +TEST_P(HttpssvcDomainParsingTest, ParseFeatureParamIntegrityDomains) { + HttpssvcExperimentDomainCache domain_cache; + + // We are not testing this feature param yet. + CHECK(!config().use_httpssvc); + + const std::string kReservedDomain = "neither.example"; + EXPECT_FALSE(domain_cache.IsExperimental(kReservedDomain)); + EXPECT_EQ(domain_cache.IsControl(kReservedDomain), + config().enabled && config().control_domain_wildcard); + + // If |config().use_integrity| is true, then we expect all domains in + // |expected_experiment_domains_| to be experimental (same goes for + // control domains). Otherwise, no domain should be considered experimental or + // control. + + if (!config().enabled) { + // When the HTTPSSVC feature is disabled, no domain should be considered + // experimental or control. + for (const std::string& experiment_domain : expected_experiment_domains_) { + EXPECT_FALSE(domain_cache.IsExperimental(experiment_domain)); + EXPECT_FALSE(domain_cache.IsControl(experiment_domain)); + } + for (const std::string& control_domain : expected_control_domains_) { + EXPECT_FALSE(domain_cache.IsExperimental(control_domain)); + EXPECT_FALSE(domain_cache.IsControl(control_domain)); + } + } else if (config().use_integrity) { + for (const std::string& experiment_domain : expected_experiment_domains_) { + EXPECT_TRUE(domain_cache.IsExperimental(experiment_domain)); + EXPECT_FALSE(domain_cache.IsControl(experiment_domain)); + } + for (const std::string& control_domain : expected_control_domains_) { + EXPECT_FALSE(domain_cache.IsExperimental(control_domain)); + EXPECT_TRUE(domain_cache.IsControl(control_domain)); + } + return; + } +} + +// Only record metrics for a non-integrity query. +TEST_P(HttpssvcMetricsTest, AddressAndIntegrityMissing) { + if (!config().enabled || !config().use_integrity) { + VerifyMetricsForExpectIntact(); + VerifyMetricsForExpectNoerror(); + return; + } + const base::TimeDelta kResolveTime = base::TimeDelta::FromMilliseconds(10); + base::Optional<HttpssvcMetrics> metrics(querying_experimental_); + metrics->SaveForNonIntegrity(base::nullopt, kResolveTime, + HttpssvcDnsRcode::kNoError); + metrics.reset(); // Record the metrics to UMA. + + VerifyMetricsForExpectIntact(); + VerifyMetricsForExpectNoerror(); +} + +TEST_P(HttpssvcMetricsTest, AddressAndIntegrityIntact) { + if (!config().enabled || !config().use_integrity) { + VerifyMetricsForExpectIntact(); + VerifyMetricsForExpectNoerror(); + return; + } + const base::TimeDelta kResolveTime = base::TimeDelta::FromMilliseconds(10); + const base::TimeDelta kResolveTimeIntegrity = + base::TimeDelta::FromMilliseconds(15); + base::Optional<HttpssvcMetrics> metrics(querying_experimental_); + metrics->SaveForIntegrity(base::nullopt, HttpssvcDnsRcode::kNoError, {true}, + kResolveTimeIntegrity); + metrics->SaveForNonIntegrity(base::nullopt, kResolveTime, + HttpssvcDnsRcode::kNoError); + metrics.reset(); // Record the metrics to UMA. + + if (querying_experimental_) { + VerifyMetricsForExpectIntact( + base::nullopt /* rcode */, {true} /* integrity */, + base::nullopt /* record_with_error */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); + + VerifyMetricsForExpectNoerror(); + return; + } + + VerifyMetricsForExpectIntact(); + + VerifyMetricsForExpectNoerror( + {HttpssvcDnsRcode::kNoError} /* rcode */, {1} /* record_received */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); +} + +// This test simulates an INTEGRITY response that includes no INTEGRITY records, +// but does have an error value for the RCODE. +TEST_P(HttpssvcMetricsTest, AddressAndIntegrityMissingWithRcode) { + if (!config().enabled || !config().use_integrity) { + VerifyMetricsForExpectIntact(); + VerifyMetricsForExpectNoerror(); + return; + } + const base::TimeDelta kResolveTime = base::TimeDelta::FromMilliseconds(10); + const base::TimeDelta kResolveTimeIntegrity = + base::TimeDelta::FromMilliseconds(15); + + base::Optional<HttpssvcMetrics> metrics(querying_experimental_); + metrics->SaveForIntegrity(base::nullopt, HttpssvcDnsRcode::kNxDomain, {}, + kResolveTimeIntegrity); + metrics->SaveForNonIntegrity(base::nullopt, kResolveTime, + HttpssvcDnsRcode::kNoError); + metrics.reset(); // Record the metrics to UMA. + + if (querying_experimental_) { + VerifyMetricsForExpectIntact( + {HttpssvcDnsRcode::kNxDomain} /* rcode */, + base::nullopt /* integrity */, base::nullopt /* record_with_error */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); + + VerifyMetricsForExpectNoerror(); + return; + } + + VerifyMetricsForExpectIntact(); + + VerifyMetricsForExpectNoerror( + {HttpssvcDnsRcode::kNxDomain} /* rcode */, + base::nullopt /* record_received */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); +} + +// This test simulates an INTEGRITY response that includes an intact INTEGRITY +// record, but also has an error RCODE. +TEST_P(HttpssvcMetricsTest, AddressAndIntegrityIntactWithRcode) { + if (!config().enabled || !config().use_integrity) { + VerifyMetricsForExpectIntact(); + VerifyMetricsForExpectNoerror(); + return; + } + + const base::TimeDelta kResolveTime = base::TimeDelta::FromMilliseconds(10); + const base::TimeDelta kResolveTimeIntegrity = + base::TimeDelta::FromMilliseconds(15); + + base::Optional<HttpssvcMetrics> metrics(querying_experimental_); + metrics->SaveForIntegrity(base::nullopt, HttpssvcDnsRcode::kNxDomain, {true}, + kResolveTimeIntegrity); + metrics->SaveForNonIntegrity(base::nullopt, kResolveTime, + HttpssvcDnsRcode::kNoError); + metrics.reset(); // Record the metrics to UMA. + + if (querying_experimental_) { + VerifyMetricsForExpectIntact( + // "DnsRcode" metric is omitted because we received an INTEGRITY record. + base::nullopt /* rcode */, + // "Integrity" metric is omitted because the RCODE is not NOERROR. + base::nullopt /* integrity */, {true} /* record_with_error */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); + + VerifyMetricsForExpectNoerror(); + return; + } + + VerifyMetricsForExpectIntact(); + + VerifyMetricsForExpectNoerror( + {HttpssvcDnsRcode::kNxDomain} /* rcode */, {true} /* record_received */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); +} + +// This test simulates an INTEGRITY response that includes a mangled INTEGRITY +// record *and* has an error RCODE. +TEST_P(HttpssvcMetricsTest, AddressAndIntegrityMangledWithRcode) { + if (!config().enabled || !config().use_integrity) { + VerifyMetricsForExpectIntact(); + VerifyMetricsForExpectNoerror(); + return; + } + const base::TimeDelta kResolveTime = base::TimeDelta::FromMilliseconds(10); + const base::TimeDelta kResolveTimeIntegrity = + base::TimeDelta::FromMilliseconds(15); + base::Optional<HttpssvcMetrics> metrics(querying_experimental_); + metrics->SaveForIntegrity(base::nullopt, HttpssvcDnsRcode::kNxDomain, {false}, + kResolveTimeIntegrity); + metrics->SaveForNonIntegrity(base::nullopt, kResolveTime, + HttpssvcDnsRcode::kNoError); + metrics.reset(); // Record the metrics to UMA. + + if (querying_experimental_) { + VerifyMetricsForExpectIntact( + // "DnsRcode" metric is omitted because we received an INTEGRITY record. + base::nullopt /* rcode */, + // "Integrity" metric is omitted because the RCODE is not NOERROR. + base::nullopt /* integrity */, {true} /* record_with_error */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); + + VerifyMetricsForExpectNoerror(); + return; + } + + VerifyMetricsForExpectIntact(); + + VerifyMetricsForExpectNoerror( + {HttpssvcDnsRcode::kNxDomain} /* rcode */, {true} /* record_received */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); +} + +// This test simulates successful address queries and an INTEGRITY query that +// timed out. +TEST_P(HttpssvcMetricsTest, AddressAndIntegrityTimedOut) { + if (!config().enabled || !config().use_integrity) { + VerifyMetricsForExpectIntact(); + VerifyMetricsForExpectNoerror(); + return; + } + const base::TimeDelta kResolveTime = base::TimeDelta::FromMilliseconds(10); + const base::TimeDelta kResolveTimeIntegrity = + base::TimeDelta::FromMilliseconds(15); + base::Optional<HttpssvcMetrics> metrics(querying_experimental_); + metrics->SaveForIntegrity(base::nullopt, HttpssvcDnsRcode::kTimedOut, {}, + kResolveTimeIntegrity); + metrics->SaveForNonIntegrity(base::nullopt, kResolveTime, + HttpssvcDnsRcode::kNoError); + metrics.reset(); // Record the metrics to UMA. + + if (querying_experimental_) { + VerifyMetricsForExpectIntact( + {HttpssvcDnsRcode::kTimedOut} /* rcode */, + // "Integrity" metric is omitted because the RCODE is not NOERROR. + base::nullopt /* integrity */, base::nullopt /* record_with_error */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); + + VerifyMetricsForExpectNoerror(); + return; + } + + VerifyMetricsForExpectIntact(); + + VerifyMetricsForExpectNoerror( + {HttpssvcDnsRcode::kTimedOut} /* rcode */, + base::nullopt /* record_received */, + {kResolveTimeIntegrity} /* resolve_time_integrity */, + {kResolveTime} /* resolve_time_non_integrity */, + {15} /* resolve_time_ratio */); +} + +} // namespace net |