blob: c74d2bfab60db8630d08686b82da81d6f72176b7 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/attribution_reporting/attribution_interop_parser.h"
#include <cmath>
#include <ostream>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "base/strings/strcat.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "base/time/time_override.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "components/attribution_reporting/source_registration.h"
#include "components/attribution_reporting/source_type.mojom.h"
#include "components/attribution_reporting/suitable_origin.h"
#include "components/attribution_reporting/trigger_registration.h"
#include "content/browser/attribution_reporting/attribution_config.h"
#include "content/browser/attribution_reporting/attribution_interop_parser.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/attribution_reporting/common_source_info.h"
#include "content/browser/attribution_reporting/storable_source.h"
#include "net/base/schemeful_site.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
namespace content {
bool operator==(const AttributionSimulationEvent& a,
const AttributionSimulationEvent& b) {
return a.event == b.event && a.debug_permission == b.debug_permission;
}
std::ostream& operator<<(std::ostream& out,
const AttributionSimulationEvent& e) {
out << "{event=";
absl::visit([&](const auto& event) { out << event; }, e.event);
return out << ",debug_permission=" << e.debug_permission << "}";
}
bool operator==(AttributionConfig::RateLimitConfig a,
AttributionConfig::RateLimitConfig b) {
const auto tie = [](AttributionConfig::RateLimitConfig config) {
return std::make_tuple(
config.time_window, config.max_source_registration_reporting_origins,
config.max_attribution_reporting_origins, config.max_attributions);
};
return tie(a) == tie(b);
}
bool operator==(AttributionConfig::EventLevelLimit a,
AttributionConfig::EventLevelLimit b) {
const auto tie = [](AttributionConfig::EventLevelLimit config) {
return std::make_tuple(config.navigation_source_trigger_data_cardinality,
config.event_source_trigger_data_cardinality,
config.randomized_response_epsilon,
config.max_reports_per_destination,
config.max_attributions_per_navigation_source,
config.max_attributions_per_event_source,
config.first_report_window_deadline,
config.second_report_window_deadline);
};
return tie(a) == tie(b);
}
bool operator==(AttributionConfig::AggregateLimit a,
AttributionConfig::AggregateLimit b) {
const auto tie = [](AttributionConfig::AggregateLimit config) {
return std::make_tuple(
config.max_reports_per_destination,
config.aggregatable_budget_per_source, config.min_delay,
config.delay_span,
config.null_reports_rate_include_source_registration_time,
config.null_reports_rate_exclude_source_registration_time,
config.max_aggregatable_reports_per_source);
};
return tie(a) == tie(b);
}
bool operator==(AttributionConfig a, AttributionConfig b) {
const auto tie = [](AttributionConfig config) {
return std::make_tuple(config.max_sources_per_origin, config.rate_limit,
config.event_level_limit, config.aggregate_limit);
};
return tie(a) == tie(b);
}
namespace {
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::attribution_reporting::SuitableOrigin;
// Pick an arbitrary offset time to test correct handling.
constexpr base::Time kOffsetTime = base::Time::UnixEpoch() + base::Days(5);
TEST(AttributionInteropParserTest, EmptyInputParses) {
const char* const kTestCases[] = {
R"json({})json",
R"json({"sources":[]})json",
R"json({"triggers":[]})json",
};
for (const char* json : kTestCases) {
base::Value::Dict value = base::test::ParseJsonDict(json);
auto result = ParseAttributionInteropInput(std::move(value), kOffsetTime);
ASSERT_TRUE(result.has_value()) << json;
EXPECT_THAT(*result, IsEmpty()) << json;
}
}
TEST(AttributionInteropParserTest, ValidSourceParses) {
constexpr char kJson[] = R"json({"sources": [
{
"timestamp": "1643235573123",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [{
"url": "https://a.r.test",
"debug_permission": true,
"response": {
"Attribution-Reporting-Register-Source": {
"destination": "https://a.d.test"
}
}
}]
},
{
"timestamp": "1643235574123",
"registration_request": {
"source_type": "event",
"attribution_src_url": "https://b.r.test",
"source_origin": "https://b.s.test",
},
"responses": [{
"url": "https://b.r.test",
"response": {
"Attribution-Reporting-Register-Source": {
"destination": "https://b.d.test"
}
}
}]
}
]})json";
base::Value::Dict value = base::test::ParseJsonDict(kJson);
auto result = ParseAttributionInteropInput(std::move(value), kOffsetTime);
ASSERT_TRUE(result.has_value()) << result.error();
ASSERT_EQ(result->size(), 2u);
const auto* source1 = absl::get_if<StorableSource>(&result->front().event);
ASSERT_TRUE(source1);
const auto* source2 = absl::get_if<StorableSource>(&result->back().event);
ASSERT_TRUE(source2);
EXPECT_EQ(result->front().time,
kOffsetTime + base::Milliseconds(1643235573123));
EXPECT_EQ(source1->common_info().source_type(),
attribution_reporting::mojom::SourceType::kNavigation);
EXPECT_EQ(source1->common_info().reporting_origin(),
*SuitableOrigin::Deserialize("https://a.r.test"));
EXPECT_EQ(source1->common_info().source_origin(),
*SuitableOrigin::Deserialize("https://a.s.test"));
EXPECT_THAT(source1->registration().destination_set.destinations(),
ElementsAre(net::SchemefulSite::Deserialize("https://d.test")));
EXPECT_FALSE(source1->is_within_fenced_frame());
EXPECT_TRUE(result->front().debug_permission);
EXPECT_EQ(result->back().time,
kOffsetTime + base::Milliseconds(1643235574123));
EXPECT_EQ(source2->common_info().source_type(),
attribution_reporting::mojom::SourceType::kEvent);
EXPECT_EQ(source2->common_info().reporting_origin(),
*SuitableOrigin::Deserialize("https://b.r.test"));
EXPECT_EQ(source2->common_info().source_origin(),
*SuitableOrigin::Deserialize("https://b.s.test"));
EXPECT_THAT(source2->registration().destination_set.destinations(),
ElementsAre(net::SchemefulSite::Deserialize("https://d.test")));
EXPECT_FALSE(source2->is_within_fenced_frame());
EXPECT_FALSE(result->back().debug_permission);
}
TEST(AttributionInteropParserTest, ValidTriggerParses) {
constexpr char kJson[] = R"json({"triggers": [
{
"timestamp": "1643235575123",
"registration_request": {
"attribution_src_url": "https://a.r.test",
"destination_origin": " https://b.d.test",
},
"responses": [{
"url": "https://a.r.test",
"debug_permission": true,
"response": {
"Attribution-Reporting-Register-Trigger": {}
}
}]
}
]})json";
base::Value::Dict value = base::test::ParseJsonDict(kJson);
auto result = ParseAttributionInteropInput(std::move(value), kOffsetTime);
ASSERT_TRUE(result.has_value());
ASSERT_EQ(result->size(), 1u);
const auto* trigger =
absl::get_if<AttributionTrigger>(&result->front().event);
ASSERT_TRUE(trigger);
EXPECT_EQ(result->front().time,
kOffsetTime + base::Milliseconds(1643235575123));
EXPECT_EQ(trigger->reporting_origin(),
*SuitableOrigin::Deserialize("https://a.r.test"));
EXPECT_EQ(trigger->destination_origin(),
*SuitableOrigin::Deserialize("https://b.d.test"));
EXPECT_EQ(trigger->verification(), absl::nullopt);
EXPECT_FALSE(trigger->is_within_fenced_frame());
EXPECT_TRUE(result->front().debug_permission);
}
struct ParseErrorTestCase {
const char* expected_failure_substr;
const char* json;
};
class AttributionInteropParserInputErrorTest
: public testing::TestWithParam<ParseErrorTestCase> {};
TEST_P(AttributionInteropParserInputErrorTest, InvalidInputFails) {
const ParseErrorTestCase& test_case = GetParam();
base::Value::Dict value = base::test::ParseJsonDict(test_case.json);
auto result = ParseAttributionInteropInput(std::move(value), kOffsetTime);
ASSERT_FALSE(result.has_value());
EXPECT_THAT(result.error(), HasSubstr(test_case.expected_failure_substr));
}
const ParseErrorTestCase kParseErrorTestCases[] = {
{
R"(["sources"]: must be a list)",
R"json({"sources": ""})json",
},
{
R"(["sources"][0]: must be a dictionary)",
R"json({"sources": [""]})json",
},
{
R"(["sources"][0]["timestamp"]: must be an integer number of)",
R"json({"sources": [{}]})json",
},
{
R"(["sources"][0]["registration_request"]: must be present)",
R"json({"sources":[{}]})json",
},
{
R"(["sources"][0]["registration_request"]: must be a dictionary)",
R"json({"sources": [{
"registration_request": ""
}]})json",
},
{
R"(["sources"][0]["registration_request"]["source_type"]: must be either)",
R"json({"sources": [{
"registration_request": {}
}]})json",
},
{
R"(["sources"][0]["registration_request"]["attribution_src_url"]: must be a valid, secure origin)",
R"json({"sources": [{
"registration_request": {}
}]})json",
},
{
R"(["sources"][0]["registration_request"]["attribution_src_url"]: must be a valid, secure origin)",
R"json({"sources": [{
"registration_request": {
"attribution_src_url": "http://r.test"
}
}]})json",
},
{
R"(["sources"][0]["registration_request"]["source_origin"]: must be a valid, secure origin)",
R"json({"sources": [{
"registration_request": {}
}]})json",
},
{
R"(["sources"][0]["registration_request"]["source_origin"]: must be a valid, secure origin)",
R"json({"sources": [{
"registration_request": {
"source_origin": "http://s.test"
}
}]})json",
},
{
R"(["sources"][0]["responses"]: must be present)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
}
}]})json",
},
{
R"(["sources"][0]["responses"]: must be a list)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": ""
}]})json",
},
{
R"(["sources"][0]["responses"]: must have size 1)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [{}, {}]
}]})json",
},
{
R"(["sources"][0]["responses"][0]: must be a dictionary)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [""]
}]})json",
},
{
R"(["sources"][0]["responses"][0]["url"]: must be a valid, secure origin)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [{}]
}]})json",
},
{
R"(["sources"][0]["responses"][0]["url"]: must match https://a.r.test)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [{
"url": "https://b.r.test"
}]
}]})json",
},
{
R"(["sources"][0]["responses"][0]["response"]: must be present)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [{
"url": "https://a.r.test"
}]
}]})json",
},
{
R"(["sources"][0]["responses"][0]["response"]: must be a dictionary)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [{
"url": "https://a.r.test",
"response": ""
}]
}]})json",
},
{
R"(["sources"][0]["responses"][0]["response"]["Attribution-Reporting-Register-Source"]: must be present)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [{
"url": "https://a.r.test",
"response": {}
}]
}]})json",
},
{
R"(["sources"][0]["responses"][0]["response"]["Attribution-Reporting-Register-Source"]: must be a dictionary)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test"
},
"responses": [{
"url": "https://a.r.test",
"response": {
"Attribution-Reporting-Register-Source": ""
}
}]
}]})json",
},
{
R"(["sources"][0]["responses"][0]["response"]["Attribution-Reporting-Register-Source"]: kDestinationMissing)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "navigation",
"attribution_src_url": "https://a.r.test",
"source_origin": "https://a.s.test",
},
"responses": [{
"url": "https://a.r.test",
"response": {
"Attribution-Reporting-Register-Source": {}
}
}]
}]})json",
},
{
R"(["sources"][0]["registration_request"]["source_type"]: must be either)",
R"json({"sources": [{
"timestamp": "1643235574000",
"registration_request": {
"source_type": "NAVIGATION"
}
}]})json",
},
{
R"(["triggers"]: must be a list)",
R"json({"triggers": ""})json",
},
{
R"(["triggers"][0]["timestamp"]: must be an integer number of)",
R"json({"triggers": [{}]})json",
},
{
R"(["triggers"][0]["registration_request"]["destination_origin"]: must be a valid, secure origin)",
R"json({"triggers": [{
"registration_request": {}
}]})json",
},
{
R"(["triggers"][0]["registration_request"]["attribution_src_url"]: must be a valid, secure origin)",
R"json({"triggers": [{
"registration_request": {}
}]})json",
},
{
R"(["triggers"][0]["responses"][0]["response"]["Attribution-Reporting-Register-Trigger"]: must be present)",
R"json({"triggers": [{
"timestamp": "1643235576000",
"registration_request": {
"destination_origin": "https://a.d1.test",
"attribution_src_url": "https://a.r.test"
},
"responses": [{
"url": "https://a.r.test",
"response": {}
}]
}]})json",
},
{
R"(["triggers"][0]["responses"][0]["response"]["Attribution-Reporting-Register-Trigger"]: must be a dictionary)",
R"json({"triggers": [{
"timestamp": "1643235576000",
"registration_request": {
"destination_origin": "https://a.d1.test",
"attribution_src_url": "https://a.r.test"
},
"responses": [{
"url": "https://a.r.test",
"response": {
"Attribution-Reporting-Register-Trigger": ""
}
}]
}]})json",
},
{
R"(["triggers"][0]["responses"][0]["response"]["Attribution-Reporting-Register-Trigger"]: kFiltersWrongType)",
R"json({"triggers": [{
"timestamp": "1643235576000",
"registration_request": {
"destination_origin": "https://a.d1.test",
"attribution_src_url": "https://a.r.test"
},
"responses": [{
"url": "https://a.r.test",
"response": {
"Attribution-Reporting-Register-Trigger": {
"filters": ""
}
}
}]
}]})json",
},
{
R"(["triggers"][1]["timestamp"]: must be distinct from all others: 1643235576000)",
R"json({"triggers": [
{
"timestamp": "1643235576000",
"registration_request": {
"destination_origin": "https://a.d1.test",
"attribution_src_url": "https://a.r.test"
},
"responses": [{
"url": "https://a.r.test",
"response": {
"Attribution-Reporting-Register-Trigger": {}
}
}]
},
{
"timestamp": "1643235576000",
"registration_request": {
"destination_origin": "https://a.d1.test",
"attribution_src_url": "https://a.r.test"
},
"responses": [{
"url": "https://a.r.test",
"response": {
"Attribution-Reporting-Register-Trigger": {}
}
}]
},
]})json",
},
};
INSTANTIATE_TEST_SUITE_P(AttributionInteropParserInvalidInputs,
AttributionInteropParserInputErrorTest,
::testing::ValuesIn(kParseErrorTestCases));
TEST(AttributionInteropParserTest, ValidConfig) {
const struct {
const char* json;
bool required;
AttributionConfig expected;
} kTestCases[] = {
{R"json({})json", false, AttributionConfig()},
{R"json({"max_sources_per_origin":"100"})json", false,
AttributionConfig{.max_sources_per_origin = 100}},
{R"json({"max_destinations_per_source_site_reporting_origin":"100"})json",
false,
AttributionConfig{.max_destinations_per_source_site_reporting_origin =
100}},
{R"json({"rate_limit_time_window":"30"})json", false,
AttributionConfig{.rate_limit = {.time_window = base::Days(30)}}},
{R"json({"rate_limit_max_source_registration_reporting_origins":"10"})json",
false,
AttributionConfig{
.rate_limit = {.max_source_registration_reporting_origins = 10}}},
{R"json({"rate_limit_max_attribution_reporting_origins":"10"})json",
false,
AttributionConfig{
.rate_limit = {.max_attribution_reporting_origins = 10}}},
{R"json({"rate_limit_max_attributions":"10"})json", false,
AttributionConfig{.rate_limit = {.max_attributions = 10}}},
{R"json({"navigation_source_trigger_data_cardinality":"10"})json", false,
AttributionConfig{
.event_level_limit = {.navigation_source_trigger_data_cardinality =
10}}},
{R"json({"event_source_trigger_data_cardinality":"10"})json", false,
AttributionConfig{
.event_level_limit = {.event_source_trigger_data_cardinality = 10}}},
{R"json({"randomized_response_epsilon":"inf"})json", false,
AttributionConfig{.event_level_limit =
{.randomized_response_epsilon =
std ::numeric_limits<double>::infinity()}}},
{R"json({"max_event_level_reports_per_destination":"10"})json", false,
AttributionConfig{
.event_level_limit = {.max_reports_per_destination = 10}}},
{R"json({"max_attributions_per_navigation_source":"10"})json", false,
AttributionConfig{
.event_level_limit = {.max_attributions_per_navigation_source =
10}}},
{R"json({"max_attributions_per_event_source":"10"})json", false,
AttributionConfig{
.event_level_limit = {.max_attributions_per_event_source = 10}}},
{R"json({"max_aggregatable_reports_per_destination":"10"})json", false,
AttributionConfig{
.aggregate_limit = {.max_reports_per_destination = 10}}},
{R"json({"aggregatable_budget_per_source":"100"})json", false,
AttributionConfig{
.aggregate_limit = {.aggregatable_budget_per_source = 100}}},
{R"json({"aggregatable_report_min_delay":"0"})json", false,
AttributionConfig{.aggregate_limit = {.min_delay = base::TimeDelta()}}},
{R"json({"aggregatable_report_delay_span":"0"})json", false,
AttributionConfig{.aggregate_limit = {.delay_span = base::TimeDelta()}}},
{R"json({
"max_sources_per_origin":"10",
"max_destinations_per_source_site_reporting_origin":"10",
"rate_limit_time_window":"10",
"rate_limit_max_source_registration_reporting_origins":"20",
"rate_limit_max_attribution_reporting_origins":"15",
"rate_limit_max_attributions":"10",
"navigation_source_trigger_data_cardinality":"100",
"event_source_trigger_data_cardinality":"10",
"randomized_response_epsilon":"0.2",
"max_event_level_reports_per_destination":"10",
"max_attributions_per_navigation_source":"5",
"max_attributions_per_event_source":"1",
"max_aggregatable_reports_per_destination":"10",
"aggregatable_budget_per_source":"1000",
"aggregatable_report_min_delay":"10",
"aggregatable_report_delay_span":"20"
})json",
true,
AttributionConfig{
.max_sources_per_origin = 10,
.max_destinations_per_source_site_reporting_origin = 10,
.rate_limit = {.time_window = base::Days(10),
.max_source_registration_reporting_origins = 20,
.max_attribution_reporting_origins = 15,
.max_attributions = 10},
.event_level_limit = {.navigation_source_trigger_data_cardinality =
100,
.event_source_trigger_data_cardinality = 10,
.randomized_response_epsilon = 0.2,
.max_reports_per_destination = 10,
.max_attributions_per_navigation_source = 5,
.max_attributions_per_event_source = 1},
.aggregate_limit = {.max_reports_per_destination = 10,
.aggregatable_budget_per_source = 1000,
.min_delay = base::Minutes(10),
.delay_span = base::Minutes(20)}}},
};
for (const auto& test_case : kTestCases) {
base::Value::Dict json = base::test::ParseJsonDict(test_case.json);
if (test_case.required) {
auto result = ParseAttributionConfig(json);
ASSERT_TRUE(result.has_value()) << json;
EXPECT_EQ(result, test_case.expected) << json;
} else {
AttributionConfig config;
EXPECT_EQ("", MergeAttributionConfig(json, config)) << json;
EXPECT_EQ(config, test_case.expected) << json;
}
}
}
TEST(AttributionInteropParserTest, InvalidConfigPositiveIntegers) {
const char* const kFields[] = {
"max_sources_per_origin",
"max_destinations_per_source_site_reporting_origin",
"rate_limit_time_window",
"rate_limit_max_source_registration_reporting_origins",
"rate_limit_max_attribution_reporting_origins",
"rate_limit_max_attributions",
"navigation_source_trigger_data_cardinality",
"event_source_trigger_data_cardinality",
"max_event_level_reports_per_destination",
"max_attributions_per_navigation_source",
"max_attributions_per_event_source",
"max_aggregatable_reports_per_destination",
"aggregatable_budget_per_source",
};
{
auto result = ParseAttributionConfig(base::Value::Dict());
ASSERT_FALSE(result.has_value());
for (const char* field : kFields) {
EXPECT_THAT(
result.error(),
HasSubstr(base::StrCat(
{"[\"", field,
"\"]: must be a positive integer formatted as base-10 string"})))
<< field;
}
}
{
AttributionConfig config;
base::Value::Dict dict;
for (const char* field : kFields) {
dict.Set(field, "0");
}
std::string error = MergeAttributionConfig(dict, config);
for (const char* field : kFields) {
EXPECT_THAT(
error,
HasSubstr(base::StrCat(
{"[\"", field,
"\"]: must be a positive integer formatted as base-10 string"})))
<< field;
}
}
}
TEST(AttributionInteropParserTest, InvalidConfigNonNegativeIntegers) {
const char* const kFields[] = {
"aggregatable_report_min_delay",
"aggregatable_report_delay_span",
};
{
auto result = ParseAttributionConfig(base::Value::Dict());
ASSERT_FALSE(result.has_value());
for (const char* field : kFields) {
EXPECT_THAT(result.error(),
HasSubstr(base::StrCat({"[\"", field,
"\"]: must be a non-negative integer "
"formatted as base-10 string"})))
<< field;
}
}
{
AttributionConfig config;
base::Value::Dict dict;
for (const char* field : kFields) {
dict.Set(field, "-10");
}
std::string error = MergeAttributionConfig(dict, config);
for (const char* field : kFields) {
EXPECT_THAT(error,
HasSubstr(base::StrCat({"[\"", field,
"\"]: must be a non-negative integer "
"formatted as base-10 string"})))
<< field;
}
}
}
TEST(AttributionInteropParserTest, InvalidConfigRandomizedResponseEpsilon) {
{
auto result = ParseAttributionConfig(base::Value::Dict());
ASSERT_FALSE(result.has_value());
EXPECT_THAT(
result.error(),
HasSubstr("[\"randomized_response_epsilon\"]: must be \"inf\" or a "
"non-negative double formated as a base-10 string"));
}
{
AttributionConfig config;
base::Value::Dict dict;
dict.Set("randomized_response_epsilon", "-1.5");
std::string error = MergeAttributionConfig(dict, config);
EXPECT_THAT(
error,
HasSubstr("[\"randomized_response_epsilon\"]: must be \"inf\" or a "
"non-negative double formated as a base-10 string"));
}
}
} // namespace
} // namespace content