blob: 6ec64e910750a944c45f6097260630faa85ed8d9 [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 "components/attribution_reporting/trigger_registration.h"
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/functional/function_ref.h"
#include "base/test/fuzztest_support.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "components/aggregation_service/aggregation_coordinator_utils.h"
#include "components/attribution_reporting/aggregatable_debug_reporting_config.h"
#include "components/attribution_reporting/aggregatable_dedup_key.h"
#include "components/attribution_reporting/aggregatable_filtering_id_max_bytes.h"
#include "components/attribution_reporting/aggregatable_named_budget_candidate.h"
#include "components/attribution_reporting/aggregatable_trigger_config.h"
#include "components/attribution_reporting/aggregatable_trigger_data.h"
#include "components/attribution_reporting/aggregatable_values.h"
#include "components/attribution_reporting/attribution_scopes_set.h"
#include "components/attribution_reporting/debug_types.mojom.h"
#include "components/attribution_reporting/event_trigger_data.h"
#include "components/attribution_reporting/filters.h"
#include "components/attribution_reporting/source_registration_time_config.mojom.h"
#include "components/attribution_reporting/suitable_origin.h"
#include "components/attribution_reporting/test_utils.h"
#include "components/attribution_reporting/trigger_registration_error.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/numeric/int128.h"
#include "third_party/fuzztest/src/fuzztest/fuzztest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace attribution_reporting {
namespace {
using ::attribution_reporting::mojom::SourceRegistrationTimeConfig;
using ::attribution_reporting::mojom::TriggerRegistrationError;
using ::base::test::ErrorIs;
using ::base::test::ValueIs;
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Property;
TriggerRegistration TriggerRegistrationWith(
base::FunctionRef<void(TriggerRegistration&)> f) {
TriggerRegistration r;
f(r);
return r;
}
TEST(TriggerRegistrationTest, Parse) {
const struct {
const char* description;
const char* json;
::testing::Matcher<
base::expected<TriggerRegistration, TriggerRegistrationError>>
matches;
} kTestCases[] = {
{
"invalid_json",
"!",
ErrorIs(TriggerRegistrationError::kInvalidJson),
},
{
"root_wrong_type",
"3",
ErrorIs(TriggerRegistrationError::kRootWrongType),
},
{
"empty",
R"json({})json",
ValueIs(AllOf(
Field(&TriggerRegistration::filters, FilterPair()),
Field(&TriggerRegistration::debug_key, std::nullopt),
Field(&TriggerRegistration::aggregatable_dedup_keys, IsEmpty()),
Field(&TriggerRegistration::event_triggers, IsEmpty()),
Field(&TriggerRegistration::aggregatable_trigger_data, IsEmpty()),
Field(&TriggerRegistration::aggregatable_values, IsEmpty()),
Field(&TriggerRegistration::debug_reporting, false),
Field(&TriggerRegistration::aggregation_coordinator_origin,
std::nullopt),
Field(&TriggerRegistration::aggregatable_trigger_config,
AggregatableTriggerConfig()),
Field(&TriggerRegistration::aggregatable_debug_reporting_config,
AggregatableDebugReportingConfig()))),
},
{
"filters_valid",
R"json({"filters":{"a":["b"], "_lookback_window": 2 }})json",
ValueIs(Field(&TriggerRegistration::filters,
FilterPair(
/*positive=*/{*FilterConfig::Create(
{{{"a", {"b"}}}},
/*lookback_window=*/base::Seconds(2))},
/*negative=*/FiltersDisjunction()))),
},
{
"filters_wrong_type",
R"json({"filters": 5})json",
ErrorIs(TriggerRegistrationError::kFiltersWrongType),
},
{
"not_filters_valid",
R"json({"not_filters":{"a":["b"]}})json",
ValueIs(Field(
&TriggerRegistration::filters,
FilterPair(
/*positive=*/FiltersDisjunction(),
/*negative=*/{*FilterConfig::Create({{{"a", {"b"}}}})}))),
},
{
"not_filters_wrong_type",
R"json({"not_filters": 5})json",
ErrorIs(TriggerRegistrationError::kFiltersWrongType),
},
{
"debug_key_valid",
R"json({"debug_key":"5"})json",
ValueIs(Field(&TriggerRegistration::debug_key, 5)),
},
{
"debug_key_invalid",
R"json({"debug_key":"-5"})json",
ValueIs(Field(&TriggerRegistration::debug_key, std::nullopt)),
},
{
"debug_key_wrong_type",
R"json({"debug_key":5})json",
ValueIs(Field(&TriggerRegistration::debug_key, std::nullopt)),
},
{
"event_triggers_valid",
R"json({"event_trigger_data":[{}, {"trigger_data":"5"}]})json",
ValueIs(Field(&TriggerRegistration::event_triggers,
ElementsAre(EventTriggerData(),
EventTriggerData(/*data=*/5, /*priority=*/0,
/*dedup_key=*/std::nullopt,
FilterPair())))),
},
{
"event_triggers_wrong_type",
R"json({"event_trigger_data":{}})json",
ErrorIs(TriggerRegistrationError::kEventTriggerDataWrongType),
},
{
"event_trigger_data_wrong_type",
R"json({"event_trigger_data":["abc"]})json",
ErrorIs(TriggerRegistrationError::kEventTriggerDataWrongType),
},
{
"event_triggers_data_invalid",
R"json({"event_trigger_data":[{"trigger_data":5}]})json",
ErrorIs(TriggerRegistrationError::kEventTriggerDataValueInvalid),
},
{
"event_triggers_priority_invalid",
R"json({"event_trigger_data": [
{
"priority":0
}
]})json",
ErrorIs(TriggerRegistrationError::kEventPriorityValueInvalid),
},
{
"event_triggers_dedup_keys_invalid",
R"json({"event_trigger_data": [
{
"deduplication_key": 1
}
]})json",
ErrorIs(TriggerRegistrationError::kEventDedupKeyValueInvalid),
},
{
"aggregatable_trigger_data_valid",
R"json({
"aggregatable_trigger_data":[
{
"key_piece": "0x1",
"source_keys": ["a"]
},
{
"key_piece": "0x2",
"source_keys": ["b"]
}
]
})json",
ValueIs(Field(&TriggerRegistration::aggregatable_trigger_data,
ElementsAre(AggregatableTriggerData(
/*key_piece=*/1,
/*source_keys=*/{"a"}, FilterPair()),
AggregatableTriggerData(
/*key_piece=*/2,
/*source_keys=*/{"b"}, FilterPair())))),
},
{
"aggregatable_trigger_data_list_wrong_type",
R"json({"aggregatable_trigger_data": {}})json",
ErrorIs(TriggerRegistrationError::kAggregatableTriggerDataWrongType),
},
{
"aggregatable_trigger_data_wrong_type",
R"json({"aggregatable_trigger_data":["abc"]})json",
ErrorIs(TriggerRegistrationError::kAggregatableTriggerDataWrongType),
},
{
"aggregatable_values_valid",
R"json({"aggregatable_values":{
"a":1, "b": {"value": 2, "filtering_id": "3"}}})json",
ValueIs(Field(&TriggerRegistration::aggregatable_values,
ElementsAre(*AggregatableValues::Create(
{{"a", *AggregatableValuesValue::Create(
1, /*filtering_id=*/0u)},
{"b", *AggregatableValuesValue::Create(2, 3)}},
FilterPair())))),
},
{
"aggregatable_values_invalid_filtering_id",
R"json({"aggregatable_values":{
"a":1,
"b": {"value": 2, "filtering_id": "256"}
}})json",
ErrorIs(TriggerRegistrationError::kAggregatableValuesValueInvalid),
},
{
"aggregatable_values_list_invalid_filtering_id",
R"json({"aggregatable_values":[{
"values": {
"a":1,
"b": {"value": 2, "filtering_id": "256" }
}
}]})json",
ErrorIs(
TriggerRegistrationError::kAggregatableValuesListValueInvalid),
},
{
"aggregatable_values_wrong_type",
R"json({"aggregatable_values":123})json",
ErrorIs(TriggerRegistrationError::kAggregatableValuesWrongType),
},
{
"debug_reporting_valid",
R"json({"debug_reporting": true})json",
ValueIs(Field(&TriggerRegistration::debug_reporting, true)),
},
{
"debug_reporting_wrong_type",
R"json({"debug_reporting":"true"})json",
ValueIs(Field(&TriggerRegistration::debug_reporting, false)),
},
{
"aggregatable_dedup_keys_valid",
R"json({
"aggregatable_deduplication_keys":[
{},
{"deduplication_key":"5"}
]
})json",
ValueIs(Field(&TriggerRegistration::aggregatable_dedup_keys,
ElementsAre(AggregatableDedupKey(),
AggregatableDedupKey(/*dedup_key=*/5,
FilterPair())))),
},
{
"aggregatable_dedup_keys_wrong_type",
R"json({"aggregatable_deduplication_keys":{}})json",
ErrorIs(TriggerRegistrationError::kAggregatableDedupKeyWrongType),
},
{
"aggregatable_dedup_key_wrong_type",
R"json({"aggregatable_deduplication_keys":["abc"]})json",
ErrorIs(TriggerRegistrationError::kAggregatableDedupKeyWrongType),
},
{
"aggregatable_dedup_key_invalid",
R"json({"aggregatable_deduplication_keys":[
{},
{"deduplication_key":5}
]})json",
ErrorIs(TriggerRegistrationError::kAggregatableDedupKeyValueInvalid),
},
{
// Tested more thoroughly in
// `aggregatable_trigger_config_unittest.cc`.
"aggregatable_source_registration_time_valid",
R"json({"aggregatable_source_registration_time":"include"})json",
ValueIs(Field(
&TriggerRegistration::aggregatable_trigger_config,
Property(
&AggregatableTriggerConfig::source_registration_time_config,
SourceRegistrationTimeConfig::kInclude))),
},
{
// Tested more thoroughly in
// `aggregatable_trigger_config_unittest.cc`.
"aggregatable_source_registration_time_invalid",
R"json({"aggregatable_source_registration_time":123})json",
ErrorIs(TriggerRegistrationError::
kAggregatableSourceRegistrationTimeValueInvalid),
},
{
// Tested more thoroughly in
// `aggregatable_filtering_id_max_bytes_unittest.cc`
"aggregatable_filtering_id_max_bytes_invalid",
R"json({"aggregatable_filtering_id_max_bytes": null})json",
ErrorIs(TriggerRegistrationError::
kAggregatableFilteringIdMaxBytesInvalidValue),
},
};
static constexpr char kTriggerRegistrationErrorMetric[] =
"Conversions.TriggerRegistrationError11";
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.description);
base::HistogramTester histograms;
auto trigger = TriggerRegistration::Parse(test_case.json);
EXPECT_THAT(trigger, test_case.matches);
if (trigger.has_value()) {
histograms.ExpectTotalCount(kTriggerRegistrationErrorMetric, 0);
} else {
histograms.ExpectUniqueSample(kTriggerRegistrationErrorMetric,
trigger.error(), 1);
}
}
}
TEST(TriggerRegistrationTest, ToJson) {
const struct {
TriggerRegistration input;
const char* expected_json;
} kTestCases[] = {
{
TriggerRegistration(),
R"json({
"aggregatable_source_registration_time": "exclude",
"aggregatable_filtering_id_max_bytes": 1,
"debug_reporting": false,
"aggregatable_debug_reporting": {
"key_piece": "0x0"
}
})json",
},
{
TriggerRegistrationWith([](TriggerRegistration& r) {
r.aggregatable_dedup_keys = {
AggregatableDedupKey(/*dedup_key=*/1, FilterPair())};
r.aggregatable_trigger_data = {AggregatableTriggerData()};
r.aggregatable_values = {
*AggregatableValues::Create(
{{"a", *AggregatableValuesValue::Create(
2, /*filtering_id=*/0u)}},
FilterPair()),
*AggregatableValues::Create(
{{"b", *AggregatableValuesValue::Create(
3, /*filtering_id=*/0u)}},
FilterPair())};
r.debug_key = 3;
r.debug_reporting = true;
r.event_triggers = {EventTriggerData()};
r.filters.positive = {*FilterConfig::Create({{{"b", {}}}})};
r.filters.negative = {*FilterConfig::Create(
{{{"c", {}}}}, /*lookback_window=*/base::Seconds(2))};
r.aggregatable_trigger_config = *AggregatableTriggerConfig::Create(
SourceRegistrationTimeConfig::kExclude,
/*trigger_context_id=*/"123",
*AggregatableFilteringIdsMaxBytes::Create(2));
r.aggregatable_debug_reporting_config =
AggregatableDebugReportingConfig(
/*key_piece=*/1,
/*debug_data=*/
{{mojom::DebugDataType::kTriggerNoMatchingSource,
*AggregatableDebugReportingContribution::Create(
/*key_piece=*/10, /*value=*/12)}},
/*aggregation_coordinator_origin=*/std::nullopt);
}),
R"json({
"aggregatable_source_registration_time": "exclude",
"aggregatable_deduplication_keys": [{"deduplication_key":"1"}],
"aggregatable_filtering_id_max_bytes": 2,
"aggregatable_trigger_data": [{"key_piece":"0x0"}],
"aggregatable_values": [
{"values":{"a": {"value": 2, "filtering_id": "0"}}},
{"values":{"b": {"value": 3, "filtering_id": "0"}}}],
"debug_key": "3",
"debug_reporting": true,
"event_trigger_data": [{"priority":"0","trigger_data":"0"}],
"filters": [{"b": []}],
"not_filters": [{"c": [], "_lookback_window": 2}],
"trigger_context_id": "123",
"aggregatable_debug_reporting": {
"key_piece": "0x1",
"debug_data": [
{
"types": ["trigger-no-matching-source"],
"key_piece": "0xa",
"value": 12
}
]
}
})json",
},
};
for (const auto& test_case : kTestCases) {
EXPECT_THAT(test_case.input.ToJson(),
base::test::IsJson(test_case.expected_json));
}
}
TEST(TriggerRegistrationTest, ParseAggregationCoordinator) {
const struct {
const char* description;
const char* json;
::testing::Matcher<
base::expected<TriggerRegistration, TriggerRegistrationError>>
matches;
} kTestCases[] = {
{
"aggregation_coordinator_origin_valid",
R"json({"aggregation_coordinator_origin":"https://a.test"})json",
ValueIs(Field(&TriggerRegistration::aggregation_coordinator_origin,
*SuitableOrigin::Create(GURL("https://a.test")))),
},
{
"aggregation_coordinator_origin_wrong_type",
R"json({"aggregation_coordinator_origin":123})json",
ErrorIs(
TriggerRegistrationError::kAggregationCoordinatorValueInvalid),
},
{
"aggregation_coordinator_origin_invalid_value",
R"json({"aggregation_coordinator_origin":"https://b.test"})json",
ErrorIs(
TriggerRegistrationError::kAggregationCoordinatorValueInvalid),
},
};
static constexpr char kTriggerRegistrationErrorMetric[] =
"Conversions.TriggerRegistrationError11";
aggregation_service::ScopedAggregationCoordinatorAllowlistForTesting
scoped_coordinator_allowlist(
{url::Origin::Create(GURL("https://a.test"))});
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.description);
base::HistogramTester histograms;
auto trigger = TriggerRegistration::Parse(test_case.json);
EXPECT_THAT(trigger, test_case.matches);
if (trigger.has_value()) {
histograms.ExpectTotalCount(kTriggerRegistrationErrorMetric, 0);
} else {
histograms.ExpectUniqueSample(kTriggerRegistrationErrorMetric,
trigger.error(), 1);
}
}
}
TEST(TriggerRegistrationTest, SerializeAggregationCoordinator) {
const struct {
TriggerRegistration input;
const char* expected_json;
} kTestCases[] = {
{
TriggerRegistration(),
R"json({
"aggregatable_filtering_id_max_bytes": 1,
"aggregatable_source_registration_time": "exclude",
"debug_reporting": false,
"aggregatable_debug_reporting": {
"key_piece": "0x0"
}
})json",
},
{
TriggerRegistrationWith([](TriggerRegistration& r) {
r.aggregation_coordinator_origin =
SuitableOrigin::Create(GURL("https://a.test"));
}),
R"json({
"aggregatable_filtering_id_max_bytes": 1,
"aggregatable_source_registration_time": "exclude",
"aggregation_coordinator_origin": "https://a.test",
"debug_reporting": false,
"aggregatable_debug_reporting": {
"key_piece": "0x0"
}
})json",
},
};
for (const auto& test_case : kTestCases) {
EXPECT_THAT(test_case.input.ToJson(),
base::test::IsJson(test_case.expected_json));
}
}
TEST(TriggerRegistrationTest, ParseAggregatableDebugReportingConfig) {
const struct {
const char* desc;
const char* json;
::testing::Matcher<
base::expected<TriggerRegistration, TriggerRegistrationError>>
matches;
} kTestCases[] = {
{
"valid",
R"json({
"aggregatable_debug_reporting": {
"key_piece": "0x2"
}
})json",
ValueIs(
Field(&TriggerRegistration::aggregatable_debug_reporting_config,
Field(&AggregatableDebugReportingConfig::key_piece, 2))),
},
{
"invalid",
R"json({
"aggregatable_debug_reporting": ""
})json",
ValueIs(
Field(&TriggerRegistration::aggregatable_debug_reporting_config,
AggregatableDebugReportingConfig())),
},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.desc);
EXPECT_THAT(TriggerRegistration::Parse(test_case.json), test_case.matches);
}
}
TEST(TriggerRegistrationTest, ParseAttributionScopesConfig) {
const struct {
const char* desc;
const char* json;
::testing::Matcher<
base::expected<TriggerRegistration, TriggerRegistrationError>>
matches;
} kTestCases[] = {
{
"attribution_scopes_set_valid",
R"json({
"attribution_scopes": ["123"]
})json",
ValueIs(Field(&TriggerRegistration::attribution_scopes,
AttributionScopesSet({"123"}))),
},
{
"empty",
R"json({})json",
ValueIs(Field(&TriggerRegistration::attribution_scopes,
AttributionScopesSet())),
},
{
"attribution_scopes_set_invalid",
R"json({
"attribution_scopes": [123]
})json",
ErrorIs(TriggerRegistrationError::kAttributionScopesValueInvalid),
},
};
for (const auto& test_case : kTestCases) {
base::HistogramTester histograms;
SCOPED_TRACE(test_case.desc);
auto trigger = TriggerRegistration::Parse(test_case.json);
EXPECT_THAT(trigger, test_case.matches);
if (trigger.has_value()) {
histograms.ExpectUniqueSample("Conversions.ScopesPerTriggerRegistration",
trigger->attribution_scopes.scopes().size(),
1);
}
}
}
TEST(TriggerRegistrationTest, ParseAggregatableNamedBudgetCandidate) {
const struct {
const char* desc;
const char* json;
::testing::Matcher<
base::expected<TriggerRegistration, TriggerRegistrationError>>
matches;
} kTestCases[] = {
{
"aggregatable_named_budgets_valid",
R"json({"named_budgets":[
{
"name":"a"
},
{
"name":"b",
"not_filters":{"a":["b"]}
}
]})json",
ValueIs(Field(
&TriggerRegistration::aggregatable_named_budget_candidates,
ElementsAre(
AggregatableNamedBudgetCandidate(/*name=*/"a", FilterPair()),
AggregatableNamedBudgetCandidate(
/*name=*/"b", FilterPair(
/*positive=*/FiltersDisjunction(),
/*negative=*/{*FilterConfig::Create(
{{{"a", {"b"}}}})}))))),
},
{
"empty",
R"json({})json",
ValueIs(
Field(&TriggerRegistration::aggregatable_named_budget_candidates,
IsEmpty())),
},
{
"aggregatable_named_budgets_invalid",
R"json({"named_budgets":[
{
"name":1
}
]})json",
ErrorIs(
TriggerRegistrationError::kAggregatableNamedBudgetNameInvalid),
},
};
for (const auto& test_case : kTestCases) {
base::HistogramTester histograms;
SCOPED_TRACE(test_case.desc);
auto trigger = TriggerRegistration::Parse(test_case.json);
EXPECT_THAT(trigger, test_case.matches);
if (trigger.has_value()) {
histograms.ExpectUniqueSample(
"Conversions.NamedBudgetsPerTriggerRegistration",
trigger->aggregatable_named_budget_candidates.size(), 1);
}
}
}
TEST(TriggerRegistrationTest, SerializeAggregatableNamedBudgetCandidate) {
const struct {
TriggerRegistration input;
const char* expected_json;
} kTestCases[] = {
{
TriggerRegistration(),
R"json({
"aggregatable_source_registration_time": "exclude",
"aggregatable_filtering_id_max_bytes": 1,
"debug_reporting": false,
"aggregatable_debug_reporting": {
"key_piece": "0x0"
}
})json",
},
{
TriggerRegistrationWith([](TriggerRegistration& r) {
r.aggregatable_named_budget_candidates = {
AggregatableNamedBudgetCandidate(
/*name=*/"a", FilterPair(
/*positive=*/FiltersDisjunction(),
/*negative=*/{*FilterConfig::Create(
{{{"a", {"b"}}}})}))};
}),
R"json({
"aggregatable_source_registration_time": "exclude",
"aggregatable_filtering_id_max_bytes": 1,
"named_budgets": [
{"name":"a", "not_filters":[{"a":["b"]}]}],
"debug_reporting": false,
"aggregatable_debug_reporting": {
"key_piece": "0x0"
}
})json",
},
};
for (const auto& test_case : kTestCases) {
EXPECT_THAT(test_case.input.ToJson(),
base::test::IsJson(test_case.expected_json));
}
}
void Parses(base::Value value) {
std::ignore = TriggerRegistration::Parse(std::move(value));
}
FUZZ_TEST(TriggerRegistrationTest, Parses)
.WithDomains(fuzztest::Arbitrary<base::Value>());
} // namespace
} // namespace attribution_reporting