| // 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 "content/browser/attribution_reporting/attribution_resolver.h" |
| |
| #include <stdint.h> |
| |
| #include <cmath> |
| #include <functional> |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/enum_set.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.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_named_budget_defs.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/aggregation_keys.h" |
| #include "components/attribution_reporting/attribution_scopes_data.h" |
| #include "components/attribution_reporting/attribution_scopes_set.h" |
| #include "components/attribution_reporting/constants.h" |
| #include "components/attribution_reporting/debug_types.mojom.h" |
| #include "components/attribution_reporting/event_report_windows.h" |
| #include "components/attribution_reporting/event_trigger_data.h" |
| #include "components/attribution_reporting/filters.h" |
| #include "components/attribution_reporting/max_event_level_reports.h" |
| #include "components/attribution_reporting/privacy_math.h" |
| #include "components/attribution_reporting/source_registration_time_config.mojom.h" |
| #include "components/attribution_reporting/source_type.mojom.h" |
| #include "components/attribution_reporting/suitable_origin.h" |
| #include "components/attribution_reporting/test_utils.h" |
| #include "components/attribution_reporting/trigger_config.h" |
| #include "components/attribution_reporting/trigger_data_matching.mojom.h" |
| #include "components/attribution_reporting/trigger_registration.h" |
| #include "content/browser/attribution_reporting/aggregatable_debug_report.h" |
| #include "content/browser/attribution_reporting/aggregatable_named_budget_pair.h" |
| #include "content/browser/attribution_reporting/attribution_features.h" |
| #include "content/browser/attribution_reporting/attribution_report.h" |
| #include "content/browser/attribution_reporting/attribution_resolver_impl.h" |
| #include "content/browser/attribution_reporting/attribution_storage_sql.h" |
| #include "content/browser/attribution_reporting/attribution_test_utils.h" |
| #include "content/browser/attribution_reporting/attribution_trigger.h" |
| #include "content/browser/attribution_reporting/common_source_info.h" |
| #include "content/browser/attribution_reporting/create_report_result.h" |
| #include "content/browser/attribution_reporting/process_aggregatable_debug_report_result.mojom.h" |
| #include "content/browser/attribution_reporting/rate_limit_result.h" |
| #include "content/browser/attribution_reporting/storable_source.h" |
| #include "content/browser/attribution_reporting/store_source_result.h" |
| #include "content/browser/attribution_reporting/stored_source.h" |
| #include "content/browser/attribution_reporting/test/configurable_storage_delegate.h" |
| #include "content/public/browser/attribution_data_model.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "net/base/schemeful_site.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/numeric/int128.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| using ::testing::_; |
| using ::testing::AllOf; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::Field; |
| using ::testing::Ge; |
| using ::testing::IsEmpty; |
| using ::testing::IsNull; |
| using ::testing::IsTrue; |
| using ::testing::Le; |
| using ::testing::Optional; |
| using ::testing::Pointee; |
| using ::testing::Property; |
| using ::testing::SizeIs; |
| using ::testing::UnorderedElementsAre; |
| using ::testing::VariantWith; |
| |
| using ::attribution_reporting::AggregatableValues; |
| using ::attribution_reporting::AggregatableValuesValue; |
| using ::attribution_reporting::FilterConfig; |
| using ::attribution_reporting::FilterData; |
| using ::attribution_reporting::FilterPair; |
| using ::attribution_reporting::kDefaultFilteringId; |
| using ::attribution_reporting::MaxEventLevelReports; |
| using ::attribution_reporting::SuitableOrigin; |
| using ::attribution_reporting::TriggerSpec; |
| using ::attribution_reporting::TriggerSpecs; |
| using ::attribution_reporting::mojom::SourceType; |
| using ::attribution_reporting::mojom::TriggerDataMatching; |
| using ::blink::mojom::AggregatableReportHistogramContribution; |
| |
| using ProcessAggregatableDebugReportStatus = |
| ::attribution_reporting::mojom::ProcessAggregatableDebugReportResult; |
| |
| // Default max number of conversions for a single impression for testing. |
| const int kMaxConversions = 3; |
| |
| // Default delay for when a report should be sent for testing. |
| constexpr base::TimeDelta kReportDelay = base::Milliseconds(5); |
| |
| StoragePartition::StorageKeyMatcherFunction GetMatcher( |
| const url::Origin& to_delete) { |
| return base::BindRepeating(std::equal_to<blink::StorageKey>(), |
| blink::StorageKey::CreateFirstParty(to_delete)); |
| } |
| |
| AggregatableDebugReport CreateAggregatableDebugReport( |
| std::vector<AggregatableReportHistogramContribution> contributions, |
| std::string_view reporting_origin = "https://r.test") { |
| return AggregatableDebugReport::CreateForTesting( |
| std::move(contributions), |
| /*context_site=*/ |
| net::SchemefulSite::Deserialize("https://c.test"), |
| /*reporting_origin=*/ |
| *SuitableOrigin::Deserialize(reporting_origin), |
| /*effective_destination=*/ |
| net::SchemefulSite::Deserialize("https://d.test"), |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| /*scheduled_report_time=*/base::Time::Now()); |
| } |
| |
| MATCHER_P(CreateReportSourceIs, matcher, "") { |
| return ExplainMatchResult(matcher, arg.source(), result_listener); |
| } |
| |
| MATCHER_P(CreateReportMaxEventLevelReportsLimitIs, matcher, "") { |
| std::optional<int> value; |
| if (const auto* v = |
| absl::get_if<CreateReportResult::NoCapacityForConversionDestination>( |
| &arg.event_level_result())) { |
| value = v->max; |
| } |
| return ExplainMatchResult(matcher, value, result_listener); |
| } |
| |
| MATCHER_P(CreateReportMaxAggregatableReportsLimitIs, matcher, "") { |
| std::optional<int> value; |
| if (const auto* v = |
| absl::get_if<CreateReportResult::NoCapacityForConversionDestination>( |
| &arg.aggregatable_result())) { |
| value = v->max; |
| } |
| return ExplainMatchResult(matcher, value, result_listener); |
| } |
| |
| MATCHER_P(SourceTimeIs, matcher, "") { |
| return ExplainMatchResult(matcher, arg.source_time(), result_listener); |
| } |
| |
| } // namespace |
| |
| // Unit test suite for the AttributionResolver interface. All |
| // AttributionResolver implementations (including fakes) should be able to |
| // re-use this test suite. |
| class AttributionResolverTest : public testing::Test { |
| public: |
| AttributionResolverTest() { |
| auto delegate = std::make_unique<ConfigurableStorageDelegate>(); |
| delegate->set_report_delay(kReportDelay); |
| delegate_ = delegate.get(); |
| // Use an empty path for an in-memory database for performance. |
| storage_ = std::make_unique<AttributionResolverImpl>(base::FilePath(), |
| std::move(delegate)); |
| } |
| |
| AttributionReport GetExpectedAggregatableReport( |
| const StoredSource& source, |
| std::vector<blink::mojom::AggregatableReportHistogramContribution> |
| contributions, |
| const AttributionTrigger& trigger) { |
| return ReportBuilder(AttributionInfoBuilder( |
| /*context_origin=*/trigger.destination_origin()) |
| .SetTime(base::Time::Now()) |
| .Build(), |
| source) |
| .SetReportTime(base::Time::Now() + kReportDelay) |
| .SetAggregatableHistogramContributions(std::move(contributions)) |
| .BuildAggregatableAttribution(); |
| } |
| |
| AttributionTrigger::EventLevelResult MaybeCreateAndStoreEventLevelReport( |
| const AttributionTrigger& conversion) { |
| return storage_->MaybeCreateAndStoreReport(conversion).event_level_status(); |
| } |
| |
| AttributionTrigger::AggregatableResult MaybeCreateAndStoreAggregatableReport( |
| const AttributionTrigger& trigger) { |
| return storage_->MaybeCreateAndStoreReport(trigger).aggregatable_status(); |
| } |
| |
| void DeleteReports(const std::vector<AttributionReport>& reports) { |
| for (const auto& report : reports) { |
| EXPECT_TRUE(storage_->DeleteReport(report.id())); |
| } |
| } |
| |
| AttributionResolver* storage() { return storage_.get(); } |
| |
| ConfigurableStorageDelegate* delegate() { return delegate_; } |
| |
| protected: |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| |
| private: |
| std::unique_ptr<AttributionResolver> storage_; |
| raw_ptr<ConfigurableStorageDelegate> delegate_; |
| }; |
| |
| TEST_F(AttributionResolverTest, ImpressionStoredAndRetrieved_ValuesIdentical) { |
| base::HistogramTester histograms; |
| storage()->StoreSource(SourceBuilder().Build()); |
| histograms.ExpectBucketCount("Conversions.DbVersionOnSourceStored", |
| AttributionStorageSql::kCurrentVersionNumber, 1); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceBuilder().BuildStored())); |
| } |
| |
| TEST_F(AttributionResolverTest, UniqueReportWindowsStored_ValuesIdentical) { |
| base::Time source_time = base::Time::Now(); |
| |
| const auto kTriggerSpecs = |
| TriggerSpecs(SourceType::kNavigation, |
| *attribution_reporting::EventReportWindows::Create( |
| /*start_time=*/base::Days(3), |
| /*end_times=*/{base::Days(15)}), |
| MaxEventLevelReports::Max()); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetExpiry(base::Days(30)) |
| .SetTriggerSpecs(kTriggerSpecs) |
| .SetAggregatableReportWindow(base::Days(5)) |
| .Build()); |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| ElementsAre(AllOf( |
| Property(&StoredSource::expiry_time, source_time + base::Days(30)), |
| Property(&StoredSource::trigger_specs, kTriggerSpecs), |
| Property(&StoredSource::aggregatable_report_window_time, |
| source_time + base::Days(5))))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| GetWithNoMatchingImpressions_NoImpressionsReturned) { |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(DefaultTrigger()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNoMatchingImpressions), |
| NewEventLevelReportIs(IsNull()), NewAggregatableReportIs(IsNull()), |
| CreateReportSourceIs(std::nullopt))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, GetWithMatchingImpression_ImpressionReturned) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, MultipleImpressionsForConversion_OneConverts) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| CrossOriginSameDomainConversion_ImpressionConverted) { |
| auto impression = |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://sub.a.test")}) |
| .Build(); |
| storage()->StoreSource(impression); |
| EXPECT_EQ( |
| AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .SetReportingOrigin(impression.common_info().reporting_origin()) |
| .Build())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ImpressionWithMultipleDestinations_ImpressionConverted) { |
| auto impression = SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://b.test"), |
| net::SchemefulSite::Deserialize("https://c.test")}) |
| .Build(); |
| storage()->StoreSource(impression); |
| EXPECT_EQ( |
| AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://c.test")) |
| .SetReportingOrigin(impression.common_info().reporting_origin()) |
| .Build())); |
| } |
| |
| TEST_F(AttributionResolverTest, EventSourceImpressionsForConversion_Converts) { |
| storage()->StoreSource( |
| SourceBuilder().SetSourceType(SourceType::kEvent).Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(TriggerBuilder().Build())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs(_))); |
| } |
| |
| TEST_F(AttributionResolverTest, ImpressionExpired_NoConversionsStored) { |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(2)).Build()); |
| task_environment_.FastForwardBy(base::Milliseconds(2)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kNoMatchingImpressions, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AggregatableReportWindowPassed_NoReportGenerated) { |
| storage()->StoreSource(TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetAggregatableReportWindow(base::Milliseconds(2)) |
| .Build()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kReportWindowPassed))); |
| } |
| |
| TEST_F(AttributionResolverTest, ImpressionExpired_ConversionsStoredPrior) { |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(4)).Build()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(5)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kNoMatchingImpressions, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ImpressionWithSetMaxConversions_ConversionReportStored) { |
| storage()->StoreSource( |
| SourceBuilder().SetMaxEventLevelReports(kMaxConversions + 1).Build()); |
| |
| for (int i = 0; i < kMaxConversions; i++) { |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| // An additional conversion report should be created. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| // No additional conversion reports should be created. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder().SetDebugKey(20).Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kPriorityTooLow), |
| ReplacedEventLevelReportIs(IsNull()), |
| DroppedEventLevelReportIs(Pointee(TriggerDebugKeyIs(20u))))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ImpressionWithMaxConversionsSetToZero_NoReportGenerated) { |
| storage()->StoreSource(SourceBuilder().SetMaxEventLevelReports(0).Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kExcessiveReports, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ImpressionReportWindowNotStarted_NoReportGenerated) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetTriggerSpecs( |
| TriggerSpecs(SourceType::kNavigation, |
| *attribution_reporting::EventReportWindows::Create( |
| base::Milliseconds(1), {base::Days(30)}), |
| MaxEventLevelReports::Max())) |
| .Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kReportWindowNotStarted, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ImpressionReportWindowsPassed_NoReportGenerated) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetTriggerSpecs( |
| TriggerSpecs(SourceType::kNavigation, |
| *attribution_reporting::EventReportWindows::Create( |
| base::Milliseconds(0), {base::Hours(1)}), |
| MaxEventLevelReports::Max())) |
| .Build()); |
| |
| task_environment_.FastForwardBy(base::Hours(1) + base::Microseconds(1)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kReportWindowPassed, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, OneConversion_OneReportScheduled) { |
| auto conversion = DefaultTrigger(); |
| |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(conversion)); |
| |
| task_environment_.FastForwardBy(kReportDelay - base::Microseconds(1)); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), IsEmpty()); |
| |
| task_environment_.FastForwardBy(base::Microseconds(1)); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), SizeIs(1u)); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ConversionWithDifferentReportingOrigin_NoReportScheduled) { |
| auto impression = SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize( |
| "https://different.test")) |
| .Build(); |
| storage()->StoreSource(impression); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kNoMatchingImpressions, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ConversionWithDifferentConversionOrigin_NoReportScheduled) { |
| auto impression = |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://different.test")}) |
| .Build(); |
| storage()->StoreSource(impression); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kNoMatchingImpressions, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, ConversionReportDeleted_RemovedFromStorage) { |
| base::HistogramTester histograms; |
| |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| std::vector<AttributionReport> reports = |
| storage()->GetAttributionReports(base::Time::Max()); |
| EXPECT_THAT(reports, SizeIs(1)); |
| DeleteReports(reports); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| histograms.ExpectTotalCount("Conversions.DbVersionOnReportSentAndDeleted", 1); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ManyImpressionsWithManyConversions_OneImpressionAttributed) { |
| const int kNumMultiTouchImpressions = 20; |
| |
| // Store a large, arbitrary number of impressions. |
| for (int i = 0; i < kNumMultiTouchImpressions; i++) { |
| storage()->StoreSource( |
| SourceBuilder().SetMaxEventLevelReports(kMaxConversions).Build()); |
| } |
| |
| for (int i = 0; i < kMaxConversions; i++) { |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| // No additional conversion reports should be created for any of the |
| // impressions. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kPriorityTooLow, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MultipleImpressionsForConversion_UnattributedImpressionsInactive) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| |
| auto new_impression = |
| SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize("https://other.test/")) |
| .Build(); |
| storage()->StoreSource(new_impression); |
| |
| // The first impression should be active because even though |
| // <reporting_origin, destination_origin> matches, it has not converted yet. |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(2)); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| } |
| |
| // This test makes sure that when a new click is received for a given |
| // <reporting_origin, destination_origin> pair, all existing impressions for |
| // that origin that have converted are marked ineligible for new conversions per |
| // the multi-touch model. |
| TEST_F(AttributionResolverTest, |
| NewImpressionForConvertedImpression_MarkedInactive) { |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(0).Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| // Delete the report. |
| DeleteReports( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max())); |
| |
| // Fast forward to ensure a later source time. |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| // Store a new impression that should mark the first inactive. |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(1000).Build()); |
| |
| // Only the new impression should convert. |
| auto conversion = DefaultTrigger(); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(conversion)); |
| |
| // Verify it was the new impression that converted. |
| EXPECT_THAT( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max()), |
| ElementsAre(EventLevelDataIs( |
| Field(&AttributionReport::EventLevelData::source_event_id, 1000u)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| NonMatchingImpressionForConvertedImpression_FirstRemainsActive) { |
| auto reporting_origin = |
| SuitableOrigin::Deserialize("https://reporter.test").value(); |
| storage()->StoreSource( |
| SourceBuilder().SetReportingOrigin(reporting_origin).Build()); |
| |
| auto conversion = TriggerBuilder().SetReportingOrigin(reporting_origin); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(conversion.Build())); |
| |
| // Store a new impression with a different reporting origin. |
| storage()->StoreSource(SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize( |
| "https://different.test")) |
| .Build()); |
| |
| // The first impression should still be active and able to convert. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(conversion.Build())); |
| |
| // Verify it was the first impression that converted. |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(ReportOriginIs(reporting_origin), |
| ReportOriginIs(reporting_origin))); |
| } |
| |
| TEST_F(AttributionResolverTest, ImpressionWithDeletedReport_RemainsActive) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| DeleteReports(storage()->GetAttributionReports(base::Time::Max())); |
| |
| // The impression should still be active and able to convert. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), SizeIs(1)); |
| } |
| |
| TEST_F( |
| AttributionResolverTest, |
| MultipleImpressionsForConversionAtDifferentTimes_OneImpressionAttributed) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| storage()->StoreSource(SourceBuilder().Build()); |
| |
| auto conversion = DefaultTrigger(); |
| |
| // Advance clock so third impression is stored at a different timestamp. |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| |
| // Make a conversion with different impression data. |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(10).Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(conversion)); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs(Field( |
| &AttributionReport::EventLevelData::source_event_id, 10)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ImpressionsAtDifferentTimes_AttributedImpressionHasCorrectReportTime) { |
| auto first_impression = SourceBuilder().Build(); |
| storage()->StoreSource(first_impression); |
| |
| // Advance clock so the next impression is stored at a different timestamp. |
| task_environment_.FastForwardBy(base::Milliseconds(2)); |
| storage()->StoreSource(SourceBuilder().Build()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(2)); |
| storage()->StoreSource(SourceBuilder().Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| // Advance to the first impression's report time and verify only its report is |
| // available. |
| task_environment_.FastForwardBy(kReportDelay - base::Milliseconds(1)); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), IsEmpty()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), SizeIs(1)); |
| } |
| |
| TEST_F(AttributionResolverTest, GetAttributionReportsMultipleTimes_SameResult) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| std::vector<AttributionReport> first_call_reports = |
| storage()->GetAttributionReports(base::Time::Now()); |
| std::vector<AttributionReport> second_call_reports = |
| storage()->GetAttributionReports(base::Time::Now()); |
| |
| // Expect that |GetAttributionReports()| did not delete any conversions. |
| EXPECT_EQ(first_call_reports, second_call_reports); |
| } |
| |
| TEST_F(AttributionResolverTest, ExceedsChannelCapacity_SupersedesRateLimits) { |
| delegate()->set_max_sources_per_origin(1); |
| EXPECT_EQ(storage()->StoreSource(SourceBuilder().Build()).status(), |
| StorableSource::Result::kSuccess); |
| |
| delegate()->set_exceeds_channel_capacity_limit(true); |
| EXPECT_EQ(storage()->StoreSource(SourceBuilder().Build()).status(), |
| StorableSource::Result::kExceedsMaxChannelCapacity); |
| } |
| |
| TEST_F(AttributionResolverTest, MaxImpressionsPerOrigin_PerOriginNotSite) { |
| delegate()->set_max_sources_per_origin(2); |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize( |
| "https://foo.a.example")) |
| .SetSourceEventId(3) |
| .Build()); |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize( |
| "https://foo.a.example")) |
| .SetSourceEventId(5) |
| .Build()); |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize( |
| "https://bar.a.example")) |
| .SetSourceEventId(7) |
| .Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceEventIdIs(3u), SourceEventIdIs(5u), |
| SourceEventIdIs(7u))); |
| |
| // This impression shouldn't be stored, because its origin has already hit the |
| // limit of 2. |
| EXPECT_EQ(storage() |
| ->StoreSource(SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize( |
| "https://foo.a.example")) |
| .SetSourceEventId(9) |
| .Build()) |
| .status(), |
| StorableSource::Result::kInsufficientSourceCapacity); |
| |
| // This impression should be stored, because its origin hasn't hit the limit |
| // of 2. |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize( |
| "https://bar.a.example")) |
| .SetSourceEventId(11) |
| .Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceEventIdIs(3u), SourceEventIdIs(5u), |
| SourceEventIdIs(7u), SourceEventIdIs(11u))); |
| } |
| |
| // Regression test for https://crbug.com/1510433 in which expired sources |
| // were erroneously counted during calculation of the sources-per-source-origin |
| // limit check. |
| TEST_F(AttributionResolverTest, MaxImpressionsPerOrigin_ExpiredSourcesIgnored) { |
| delegate()->set_max_sources_per_origin(1); |
| |
| // Effectively prevent expired sources from being deleted/deactivated. |
| delegate()->set_delete_expired_sources_frequency(base::TimeDelta::Max()); |
| |
| const auto kSourceOrigin = *SuitableOrigin::Deserialize("https://a.example"); |
| constexpr base::TimeDelta kExpiry = base::Days(1); |
| |
| ASSERT_EQ(StorableSource::Result::kSuccess, |
| storage() |
| ->StoreSource(SourceBuilder() |
| .SetSourceOrigin(kSourceOrigin) |
| .SetSourceEventId(111) |
| .SetExpiry(kExpiry) |
| .Build()) |
| .status()); |
| |
| ASSERT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceEventIdIs(111u))); |
| |
| task_environment_.FastForwardBy(kExpiry); |
| |
| // This source *should* be stored successfully, as the previous source has |
| // expired at this point. |
| ASSERT_EQ(StorableSource::Result::kSuccess, |
| storage() |
| ->StoreSource(SourceBuilder() |
| .SetSourceOrigin(kSourceOrigin) |
| .SetSourceEventId(222) |
| .Build()) |
| .status()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceEventIdIs(222u))); |
| } |
| |
| TEST_F(AttributionResolverTest, MaxEventLevelReportsPerDestination) { |
| SourceBuilder source_builder = TestAggregatableSourceProvider().GetBuilder(); |
| |
| delegate()->set_max_reports_per_destination( |
| AttributionReport::Type::kEventLevel, 1); |
| storage()->StoreSource(source_builder.Build()); |
| storage()->StoreSource(source_builder.Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| CreateReportMaxEventLevelReportsLimitIs(std::nullopt), |
| CreateReportMaxAggregatableReportsLimitIs(std::nullopt))); |
| |
| // Verify that MaxReportsPerDestination is enforced. |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult:: |
| kNoCapacityForConversionDestination), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| ReplacedEventLevelReportIs(IsNull()), |
| DroppedEventLevelReportIs(IsNull()), |
| CreateReportMaxEventLevelReportsLimitIs(1), |
| CreateReportMaxAggregatableReportsLimitIs(std::nullopt))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxEventLevelReportsPerDestination_MultipleDestinations) { |
| SourceBuilder source_builder = TestAggregatableSourceProvider().GetBuilder(); |
| |
| delegate()->set_max_reports_per_destination( |
| AttributionReport::Type::kEventLevel, 1); |
| storage()->StoreSource( |
| source_builder |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://b.test")}) |
| .Build()); |
| storage()->StoreSource( |
| source_builder |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://c.test")}) |
| .Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| CreateReportMaxEventLevelReportsLimitIs(std::nullopt), |
| CreateReportMaxAggregatableReportsLimitIs(std::nullopt))); |
| |
| // Verify that MaxReportsPerDestination is enforced. |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult:: |
| kNoCapacityForConversionDestination), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| ReplacedEventLevelReportIs(IsNull()), |
| DroppedEventLevelReportIs(IsNull()), |
| CreateReportMaxEventLevelReportsLimitIs(1), |
| CreateReportMaxAggregatableReportsLimitIs(std::nullopt))); |
| } |
| |
| TEST_F(AttributionResolverTest, MaxAggregatableReportsPerDestination) { |
| SourceBuilder source_builder = TestAggregatableSourceProvider().GetBuilder(); |
| |
| delegate()->set_max_reports_per_destination( |
| AttributionReport::Type::kAggregatableAttribution, 1); |
| storage()->StoreSource(source_builder.Build()); |
| storage()->StoreSource(source_builder.Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| CreateReportMaxEventLevelReportsLimitIs(std::nullopt), |
| CreateReportMaxAggregatableReportsLimitIs(std::nullopt))); |
| |
| // Verify that MaxReportsPerDestination is enforced. |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult:: |
| kNoCapacityForConversionDestination), |
| ReplacedEventLevelReportIs(IsNull()), |
| DroppedEventLevelReportIs(IsNull()), |
| CreateReportMaxEventLevelReportsLimitIs(std::nullopt), |
| CreateReportMaxAggregatableReportsLimitIs(1))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxAggregatableReportsPerDestination_MultipleDestinations) { |
| SourceBuilder source_builder = TestAggregatableSourceProvider().GetBuilder(); |
| |
| delegate()->set_max_reports_per_destination( |
| AttributionReport::Type::kAggregatableAttribution, 1); |
| storage()->StoreSource( |
| source_builder |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://b.test")}) |
| .Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .Build()), |
| AllOf(CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| // Verify that only the effective destination is used for the limit in |
| // aggregatable reports. |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://b.test")) |
| .Build()), |
| AllOf(CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| // Verify that MaxReportsPerDestination is enforced. |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .Build()), |
| AllOf(CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult:: |
| kNoCapacityForConversionDestination), |
| CreateReportMaxAggregatableReportsLimitIs(1))); |
| } |
| |
| TEST_F(AttributionResolverTest, ClearDataWithNoMatch_NoDelete) { |
| base::Time now = base::Time::Now(); |
| storage()->StoreSource(SourceBuilder(now).Build()); |
| storage()->ClearData( |
| now, now, GetMatcher(url::Origin::Create(GURL("https://no-match.com")))); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ClearData_SourceAndDestinationOriginsIrrelevant) { |
| const auto origin = *SuitableOrigin::Deserialize("https://a.test"); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceOrigin(origin) |
| .SetDestinationSites({net::SchemefulSite(origin)}) |
| .Build()); |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetDestinationOrigin(origin).Build()); |
| |
| ASSERT_EQ(storage()->GetActiveSources().size(), 1u); |
| ASSERT_EQ(storage()->GetAttributionReports(base::Time::Max()).size(), 1u); |
| |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| GetMatcher(*origin)); |
| |
| EXPECT_EQ(storage()->GetActiveSources().size(), 1u); |
| EXPECT_EQ(storage()->GetAttributionReports(base::Time::Max()).size(), 1u); |
| } |
| |
| TEST_F(AttributionResolverTest, ClearDataOutsideRange_NoDelete) { |
| base::Time now = base::Time::Now(); |
| auto impression = SourceBuilder(now).Build(); |
| storage()->StoreSource(impression); |
| |
| storage()->ClearData(now + base::Minutes(10), now + base::Minutes(20), |
| GetMatcher(impression.common_info().reporting_origin())); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, ClearDataImpression) { |
| base::Time now = base::Time::Now(); |
| |
| { |
| auto impression = SourceBuilder(now).Build(); |
| storage()->StoreSource(impression); |
| storage()->ClearData( |
| now, now + base::Minutes(20), |
| GetMatcher(impression.common_info().reporting_origin())); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kNoMatchingImpressions, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| } |
| |
| TEST_F(AttributionResolverTest, ClearDataImpressionConversion) { |
| base::Time now = base::Time::Now(); |
| auto impression = SourceBuilder(now).Build(); |
| auto conversion = DefaultTrigger(); |
| |
| storage()->StoreSource(impression); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(conversion)); |
| |
| storage()->ClearData(now - base::Minutes(20), now + base::Minutes(20), |
| GetMatcher(impression.common_info().reporting_origin())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| // The null filter should match all origins. |
| TEST_F(AttributionResolverTest, ClearDataNullFilter) { |
| base::Time now = base::Time::Now(); |
| |
| for (int i = 0; i < 10; i++) { |
| auto origin = |
| *SuitableOrigin::Deserialize(base::StringPrintf("https://%d.com/", i)); |
| storage()->StoreSource( |
| SourceBuilder(now) |
| .SetExpiry(base::Days(30)) |
| .SetSourceOrigin(origin) |
| .SetReportingOrigin(origin) |
| .SetDestinationSites({net::SchemefulSite(origin)}) |
| .Build()); |
| task_environment_.FastForwardBy(base::Days(1)); |
| } |
| |
| // Convert half of them now, half after another day. |
| for (int i = 0; i < 5; i++) { |
| auto origin = |
| *SuitableOrigin::Deserialize(base::StringPrintf("https://%d.com/", i)); |
| EXPECT_EQ( |
| AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(TriggerBuilder() |
| .SetDestinationOrigin(origin) |
| .SetReportingOrigin(origin) |
| .Build())); |
| } |
| task_environment_.FastForwardBy(base::Days(1)); |
| for (int i = 5; i < 10; i++) { |
| auto origin = |
| *SuitableOrigin::Deserialize(base::StringPrintf("https://%d.com/", i)); |
| EXPECT_EQ( |
| AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(TriggerBuilder() |
| .SetDestinationOrigin(origin) |
| .SetReportingOrigin(origin) |
| .Build())); |
| } |
| |
| auto null_filter = StoragePartition::StorageKeyMatcherFunction(); |
| storage()->ClearData(base::Time::Now(), base::Time::Now(), null_filter); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), SizeIs(5)); |
| } |
| |
| TEST_F(AttributionResolverTest, ClearDataWithImpressionOutsideRange) { |
| base::Time start = base::Time::Now(); |
| auto impression = SourceBuilder(start).SetExpiry(base::Days(30)).Build(); |
| auto conversion = DefaultTrigger(); |
| |
| storage()->StoreSource(impression); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(conversion)); |
| storage()->ClearData(base::Time::Now(), base::Time::Now(), |
| GetMatcher(impression.common_info().reporting_origin())); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| // Deletions with time range between the impression and conversion should not |
| // delete anything, unless the time range intersects one of the events. |
| TEST_F(AttributionResolverTest, ClearDataRangeBetweenEvents) { |
| base::Time start = base::Time::Now(); |
| |
| auto impression = SourceBuilder().SetExpiry(base::Days(30)).Build(); |
| auto conversion = DefaultTrigger(); |
| |
| storage()->StoreSource(impression); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(conversion)); |
| |
| storage()->ClearData(start + base::Minutes(1), start + base::Minutes(10), |
| GetMatcher(impression.common_info().reporting_origin())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), SizeIs(1u)); |
| } |
| |
| // Test that only a subset of impressions / conversions are deleted with |
| // multiple impressions per conversion, if only a subset of impressions match. |
| TEST_F(AttributionResolverTest, ClearDataWithMultiTouch) { |
| base::Time start = base::Time::Now(); |
| auto impression1 = SourceBuilder(start).SetExpiry(base::Days(30)).Build(); |
| storage()->StoreSource(impression1); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| storage()->StoreSource(SourceBuilder().SetExpiry(base::Days(30)).Build()); |
| storage()->StoreSource(SourceBuilder().SetExpiry(base::Days(30)).Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| // Only the first impression should overlap with this time range, but all the |
| // impressions should share the origin. |
| storage()->ClearData( |
| start, start, GetMatcher(impression1.common_info().reporting_origin())); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), SizeIs(1)); |
| } |
| |
| // The max time range with a null filter should delete everything. |
| TEST_F(AttributionResolverTest, DeleteAll) { |
| base::Time start = base::Time::Now(); |
| for (int i = 0; i < 10; i++) { |
| storage()->StoreSource( |
| SourceBuilder(start).SetExpiry(base::Days(30)).Build()); |
| task_environment_.FastForwardBy(base::Days(1)); |
| } |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| auto null_filter = StoragePartition::StorageKeyMatcherFunction(); |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), null_filter); |
| |
| // Verify that everything is deleted. |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| // Same as the above test, but uses base::Time() instead of base::Time::Min() |
| // for delete_begin, which should yield the same behavior. |
| TEST_F(AttributionResolverTest, DeleteAllNullDeleteBegin) { |
| base::Time start = base::Time::Now(); |
| for (int i = 0; i < 10; i++) { |
| storage()->StoreSource( |
| SourceBuilder(start).SetExpiry(base::Days(30)).Build()); |
| task_environment_.FastForwardBy(base::Days(1)); |
| } |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| auto null_filter = StoragePartition::StorageKeyMatcherFunction(); |
| storage()->ClearData(base::Time(), base::Time::Max(), null_filter); |
| |
| // Verify that everything is deleted. |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, MaxAttributionsBetweenSites) { |
| base::HistogramTester histogram_tester; |
| |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = base::TimeDelta::Max(); |
| r.max_source_registration_reporting_origins = |
| std::numeric_limits<int64_t>::max(); |
| r.max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(); |
| r.max_attributions = 2; |
| return r; |
| }()); |
| |
| SourceBuilder source_builder = TestAggregatableSourceProvider().GetBuilder(); |
| storage()->StoreSource(source_builder.Build()); |
| |
| auto conversion1 = TriggerBuilder().SetTriggerData(1).Build(); |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(conversion1), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kNotRegistered))); |
| |
| auto conversion2 = DefaultAggregatableTriggerBuilder(/*histogram_values=*/{5}) |
| .SetTriggerData(2) |
| .Build(); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(conversion2), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| auto conversion3 = DefaultAggregatableTriggerBuilder(/*histogram_values=*/{3}) |
| .SetTriggerData(3) |
| .Build(); |
| |
| // Event-level reports and aggregatable reports don't share the attribution |
| // limit. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(conversion3), |
| AllOf( |
| Property(&CreateReportResult::event_level_result, |
| VariantWith<CreateReportResult::ExcessiveAttributions>(Field( |
| &CreateReportResult::ExcessiveAttributions::max, 2))), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| ReplacedEventLevelReportIs(IsNull()), |
| DroppedEventLevelReportIs(IsNull()))); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(conversion3), |
| AllOf( |
| Property(&CreateReportResult::event_level_result, |
| VariantWith<CreateReportResult::ExcessiveAttributions>(Field( |
| &CreateReportResult::ExcessiveAttributions::max, 2))), |
| Property(&CreateReportResult::aggregatable_result, |
| VariantWith<CreateReportResult::ExcessiveAttributions>(Field( |
| &CreateReportResult::ExcessiveAttributions::max, 2))), |
| ReplacedEventLevelReportIs(IsNull()), |
| DroppedEventLevelReportIs(IsNull()))); |
| |
| const auto source = |
| source_builder.SetRemainingAggregatableAttributionBudget(65536 - 8) |
| .BuildStored(); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs(TriggerDataIs(1)), |
| EventLevelDataIs(TriggerDataIs(2)), |
| GetExpectedAggregatableReport( |
| source, |
| DefaultAggregatableHistogramContributions( |
| /*histogram_values=*/{5}), |
| conversion2), |
| GetExpectedAggregatableReport( |
| source, |
| DefaultAggregatableHistogramContributions( |
| /*histogram_values=*/{3}), |
| conversion3))); |
| |
| // kEventLevelOnly = 0, kAggregatableOnly = 1, kBoth = 2. |
| EXPECT_THAT(histogram_tester.GetAllSamples("Conversions.AttributionResult"), |
| base::BucketsAre(base::Bucket(0, 1), base::Bucket(1, 1), |
| base::Bucket(2, 1))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxAttributionReportsBetweenSites_IgnoresSourceType) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = base::TimeDelta::Max(); |
| r.max_source_registration_reporting_origins = |
| std::numeric_limits<int64_t>::max(); |
| r.max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(); |
| r.max_attributions = 1; |
| return r; |
| }()); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetSourceType(SourceType::kNavigation).Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetSourceType(SourceType::kEvent).Build()); |
| // This would fail if the source types had separate limits. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kExcessiveAttributions, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| NeverAttributeImpression_EventLevelReportNotStored) { |
| delegate()->set_randomized_response( |
| std::vector<attribution_reporting::FakeEventLevelReport>{}); |
| StoreSourceResult result = storage()->StoreSource( |
| TestAggregatableSourceProvider().GetBuilder().Build()); |
| EXPECT_EQ(result.status(), StorableSource::Result::kSuccessNoised); |
| delegate()->set_randomized_response(std::nullopt); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNeverAttributedSource), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(AggregatableAttributionDataIs( |
| AggregatableHistogramContributionsAre( |
| DefaultAggregatableHistogramContributions())))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AttributeFalseImpression_OtherSourceDeactivated) { |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(7).Build()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| delegate()->set_randomized_response( |
| std::vector<attribution_reporting::FakeEventLevelReport>{ |
| {.trigger_data = 7, .window_index = 0}}); |
| StoreSourceResult result = |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(5).Build()); |
| EXPECT_EQ(result.status(), StorableSource::Result::kSuccessNoised); |
| delegate()->set_randomized_response(std::nullopt); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(2u)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kFalselyAttributedSource, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), ElementsAre(SourceEventIdIs(5u))); |
| } |
| |
| TEST_F(AttributionResolverTest, NeverAttributeImpression_RateLimitsChanged) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = base::TimeDelta::Max(); |
| r.max_source_registration_reporting_origins = |
| std::numeric_limits<int64_t>::max(); |
| r.max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(); |
| r.max_attributions = 1; |
| return r; |
| }()); |
| |
| delegate()->set_randomized_response( |
| std::vector<attribution_reporting::FakeEventLevelReport>{}); |
| storage()->StoreSource(TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetSourceEventId(5) |
| .Build()); |
| delegate()->set_randomized_response(std::nullopt); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kExcessiveAttributions), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| NeverAndTruthfullyAttributeImpressions_EventLevelReportNotStored) { |
| TestAggregatableSourceProvider provider; |
| |
| storage()->StoreSource(provider.GetBuilder().Build()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| delegate()->set_randomized_response( |
| std::vector<attribution_reporting::FakeEventLevelReport>{}); |
| |
| storage()->StoreSource(provider.GetBuilder().Build()); |
| delegate()->set_randomized_response(std::nullopt); |
| |
| const auto conversion = DefaultAggregatableTriggerBuilder().Build(); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(conversion), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNeverAttributedSource), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(conversion), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNeverAttributedSource), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| auto contributions = DefaultAggregatableHistogramContributions(); |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(AggregatableAttributionDataIs( |
| AggregatableHistogramContributionsAre(contributions)), |
| AggregatableAttributionDataIs( |
| AggregatableHistogramContributionsAre(contributions)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxDestinationsPerSource_ScopedToSourceSiteAndReportingSite) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(3); |
| |
| const auto store_source = [&](const char* source_origin, |
| const char* reporting_origin, |
| const char* destination_origin, |
| int64_t destination_limit_priority = 0) { |
| return storage() |
| ->StoreSource( |
| SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize(source_origin)) |
| .SetReportingOrigin( |
| *SuitableOrigin::Deserialize(reporting_origin)) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize(destination_origin)}) |
| .SetExpiry(base::Days(30)) |
| .SetDestinationLimitPriority(destination_limit_priority) |
| .Build()) |
| .status(); |
| }; |
| |
| store_source("https://s1.test", "https://a.r.test", "https://d1.test"); |
| store_source("https://s1.test", "https://a.r.test", "https://d2.test"); |
| store_source("https://s1.test", "https://a.r.test", "https://d3.test"); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(3)); |
| |
| // This should succeed because the destination is already present on an |
| // unexpired source. |
| store_source("https://s1.test", "https://a.r.test", "https://d2.test"); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(4)); |
| |
| // This should fail because there are already 3 distinct destinations. |
| EXPECT_EQ(store_source("https://s1.test", "https://a.r.test", |
| "https://d4.test", /*destination_limit_priority=*/-1), |
| StorableSource::Result::kInsufficientUniqueDestinationCapacity); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(4)); |
| |
| // This should succeed because the source site is different. |
| store_source("https://s2.test", "https://a.r.test", "https://d5.test"); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(5)); |
| |
| // This should fail because the reporting site is already present. |
| store_source("https://s1.test", "https://b.r.test", "https://d5.test", |
| /*destination_limit_priority=*/-1); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(5)); |
| |
| // This should succeed because the reporting site is different. |
| store_source("https://s1.test", "https://a.r1.test", "https://d5.test"); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(6)); |
| } |
| |
| TEST_F(AttributionResolverTest, DestinationLimit_ApplyLimit) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| delegate()->set_delete_expired_sources_frequency(base::Milliseconds(10)); |
| |
| const base::TimeDelta expiry = kReportDelay + base::Milliseconds(1); |
| |
| const auto store_source = [&](const char* source_origin, |
| const char* reporting_origin, |
| const char* destination_origin, |
| int64_t destination_limit_priority = 0) { |
| return storage() |
| ->StoreSource( |
| SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize(source_origin)) |
| .SetReportingOrigin( |
| *SuitableOrigin::Deserialize(reporting_origin)) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize(destination_origin)}) |
| .SetExpiry(expiry) |
| .SetDestinationLimitPriority(destination_limit_priority) |
| .Build()) |
| .status(); |
| }; |
| |
| EXPECT_EQ( |
| store_source("https://s.test", "https://a.r.test", "https://d1.test"), |
| StorableSource::Result::kSuccess); |
| |
| EXPECT_EQ(store_source("https://s.test", "https://a.r.test", |
| "https://d2.test", /*destination_limit_priority=*/-1), |
| StorableSource::Result::kInsufficientUniqueDestinationCapacity); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetReportingOrigin( |
| *SuitableOrigin::Deserialize("https://a.r.test")) |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://d1.test")) |
| .Build())); |
| |
| // The first source is still counted after being attributed to. |
| EXPECT_EQ(store_source("https://s.test", "https://a.r.test", |
| "https://d2.test", /*destination_limit_priority=*/-1), |
| StorableSource::Result::kInsufficientUniqueDestinationCapacity); |
| |
| task_environment_.FastForwardBy(expiry); |
| |
| EXPECT_EQ(store_source("https://s.test", "https://a.r.test", |
| "https://d3.test", /*destination_limit_priority=*/-1), |
| StorableSource::Result::kSuccess); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxAttributionDestinationsPerSource_AppliesToNavigationSources) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example/")}) |
| .Build()); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.example")}) |
| .Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxAttributionDestinationsPerSource_CountsAllSourceTypes) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example/")}) |
| .SetSourceType(SourceType::kNavigation) |
| .Build()); |
| StoreSourceResult result = storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.example")}) |
| .SetSourceType(SourceType::kEvent) |
| .SetDestinationLimitPriority(-1) |
| .Build()); |
| |
| EXPECT_THAT( |
| result.result(), |
| VariantWith<StoreSourceResult::InsufficientUniqueDestinationCapacity>( |
| Field( |
| &StoreSourceResult::InsufficientUniqueDestinationCapacity::limit, |
| 1))); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxAttributionDestinationsPerSource_CountsUnexpiredSources) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| delegate()->set_delete_expired_rate_limits_frequency(base::Milliseconds(10)); |
| |
| const base::TimeDelta expiry = kReportDelay + base::Milliseconds(1); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example/")}) |
| .SetSourceType(SourceType::kNavigation) |
| .SetExpiry(expiry) |
| .Build()); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.example")}) |
| .SetSourceType(SourceType::kEvent) |
| .SetDestinationLimitPriority(-1) |
| .Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| |
| task_environment_.FastForwardBy(expiry); |
| EXPECT_THAT(storage()->GetActiveSources(), IsEmpty()); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.example")}) |
| .SetSourceType(SourceType::kEvent) |
| .Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxAttributionDestinationsPerSource_SourceWithTooManyDestinations) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example/"), |
| net::SchemefulSite::Deserialize("https://b.example/")}) |
| .SetSourceType(SourceType::kNavigation) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), IsEmpty()); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://c.example")}) |
| .SetSourceType(SourceType::kEvent) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| HandleSource_DestinationThrottleReportingLimitReached) { |
| // Current reporting limit for Destination Throttle |
| int max_per_reporting_source_site = 50; |
| |
| for (int i = 0; i < max_per_reporting_source_site; i++) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d" + base::NumberToString(i) + ".test")}) |
| .Build()); |
| } |
| EXPECT_THAT(storage()->GetActiveSources(), |
| SizeIs(max_per_reporting_source_site)); |
| |
| // Should fail due to limit |
| StoreSourceResult result = storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_THAT(result.status(), |
| StorableSource::Result::kDestinationReportingLimitReached); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| HandleSource_DestinationThrottleGlobalLimitReached) { |
| // Current global limit for Destination Throttle |
| int max_global_source_site = 200; |
| for (int i = 0; i < max_global_source_site; i++) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize( |
| "https://r" + base::NumberToString(i) + ".test")) |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d" + base::NumberToString(i) + ".test")}) |
| .Build()); |
| } |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(max_global_source_site)); |
| |
| // Should fail due to limit |
| StoreSourceResult result = storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_THAT(result.status(), |
| StorableSource::Result::kDestinationGlobalLimitReached); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| HandleSource_DestinationThrottleBothLimitsReached) { |
| int max_global_source_site = 200; |
| for (int i = 0; i < max_global_source_site; i += 4) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r1.test")) |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d" + base::NumberToString(i) + ".test")}) |
| .Build()); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r2.test")) |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d" + base::NumberToString(i + 1) + ".test")}) |
| .Build()); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r3.test")) |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d" + base::NumberToString(i + 2) + ".test")}) |
| .Build()); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r4.test")) |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d" + base::NumberToString(i + 3) + ".test")}) |
| .Build()); |
| } |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(max_global_source_site)); |
| |
| // Should fail due to limit |
| StoreSourceResult result = storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r1.test")) |
| .Build()); |
| EXPECT_THAT(result.status(), |
| StorableSource::Result::kDestinationBothLimitsReached); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MultipleImpressionsPerConversion_MostRecentAttributesForSamePriority) { |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(3).Build()); |
| |
| // Note: Fast-forwards aren't necessary here because source order for the |
| // purposes of prioritization is based on rowid, not source time. |
| |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(7).Build()); |
| |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(5).Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(3)); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs(Field( |
| &AttributionReport::EventLevelData::source_event_id, 5u)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MultipleImpressionsPerConversion_HighestPriorityAttributes) { |
| storage()->StoreSource( |
| SourceBuilder().SetPriority(100).SetSourceEventId(3).Build()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| storage()->StoreSource( |
| SourceBuilder().SetPriority(300).SetSourceEventId(5).Build()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| storage()->StoreSource( |
| SourceBuilder().SetPriority(200).SetSourceEventId(7).Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(3)); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs(Field( |
| &AttributionReport::EventLevelData::source_event_id, 5u)))); |
| } |
| |
| TEST_F(AttributionResolverTest, MultipleImpressions_CorrectDeactivation) { |
| storage()->StoreSource( |
| SourceBuilder().SetSourceEventId(3).SetPriority(0).Build()); |
| storage()->StoreSource( |
| SourceBuilder().SetSourceEventId(5).SetPriority(1).Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(2)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| // Because the impression with data 5 has the highest priority, it is selected |
| // for attribution. The unselected impression with data 3 should be |
| // deactivated, but the one with data 5 should remain active. |
| EXPECT_THAT(storage()->GetActiveSources(), ElementsAre(SourceEventIdIs(5u))); |
| } |
| |
| TEST_F(AttributionResolverTest, FalselyAttributeImpression_ReportStored) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = base::TimeDelta::Max(); |
| r.max_source_registration_reporting_origins = |
| std::numeric_limits<int64_t>::max(); |
| r.max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(); |
| r.max_attributions = 2; |
| return r; |
| }()); |
| |
| base::TimeDelta kFirstWindow = base::Days(1); |
| base::TimeDelta kExpiry = base::Days(30); |
| const base::Time fake_report_time = base::Time::Now() + kFirstWindow; |
| const base::Time fake_trigger_time = base::Time::Now(); |
| |
| SourceBuilder builder = TestAggregatableSourceProvider().GetBuilder(); |
| builder.SetSourceEventId(4) |
| .SetSourceType(SourceType::kEvent) |
| .SetExpiry(kExpiry) |
| .SetPriority(100) |
| .SetTriggerSpecs( |
| TriggerSpecs(SourceType::kEvent, |
| *attribution_reporting::EventReportWindows::Create( |
| base::Days(0), {kFirstWindow, kExpiry}), |
| MaxEventLevelReports(1))); |
| delegate()->set_randomized_response( |
| std::vector<attribution_reporting::FakeEventLevelReport>{ |
| {.trigger_data = 1, .window_index = 0}}); |
| StoreSourceResult result = storage()->StoreSource(builder.Build()); |
| EXPECT_EQ(result.status(), StorableSource::Result::kSuccessNoised); |
| delegate()->set_randomized_response(std::nullopt); |
| |
| AttributionReport expected_event_level_report = |
| ReportBuilder( |
| AttributionInfoBuilder( |
| /*context_origin=*/*SuitableOrigin::Deserialize( |
| "https://impression.test")) |
| .SetTime(fake_trigger_time) |
| .Build(), |
| builder.SetAttributionLogic(StoredSource::AttributionLogic::kFalsely) |
| .SetExpiry(kExpiry) |
| .SetActiveState( |
| StoredSource::ActiveState::kReachedEventLevelAttributionLimit) |
| .SetMaxEventLevelReports(1) |
| .BuildStored()) |
| .SetTriggerData(1) |
| .SetReportTime(fake_report_time) |
| .Build(); |
| |
| task_environment_.FastForwardBy(kFirstWindow); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), |
| ElementsAre(expected_event_level_report)); |
| |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| ElementsAre(SourceActiveStateIs( |
| StoredSource::ActiveState::kReachedEventLevelAttributionLimit))); |
| |
| AttributionTrigger trigger = DefaultAggregatableTriggerBuilder().Build(); |
| |
| // The falsely attributed impression should only be eligible for further |
| // aggregatable reports, but not event-level reports. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(trigger), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kFalselyAttributedSource), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| // Event-level and aggregatable attribution rate limits are separate. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(trigger), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kFalselyAttributedSource), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| // The source's aggregatable budget consumed changes between the two |
| // GetAttributionReports() calls due to the aggregatable trigger, which |
| // requires a reflection of that change within the event level report |
| // for the test to pass. |
| expected_event_level_report = |
| ReportBuilder( |
| AttributionInfoBuilder( |
| /*context_origin=*/*SuitableOrigin::Deserialize( |
| "https://impression.test")) |
| .SetTime(fake_trigger_time) |
| .Build(), |
| builder.SetAttributionLogic(StoredSource::AttributionLogic::kFalsely) |
| .SetRemainingAggregatableAttributionBudget(65536 - 2) |
| .SetExpiry(kExpiry) |
| .SetActiveState( |
| StoredSource::ActiveState::kReachedEventLevelAttributionLimit) |
| .BuildStored()) |
| .SetTriggerData(1) |
| .SetReportTime(fake_report_time) |
| .Build(); |
| |
| const AttributionReport expected_aggregatable_report = |
| GetExpectedAggregatableReport( |
| builder.SetRemainingAggregatableAttributionBudget(65536 - 2) |
| .BuildStored(), |
| DefaultAggregatableHistogramContributions({1}), trigger); |
| |
| task_environment_.FastForwardBy(kExpiry); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Now()), |
| ElementsAre(expected_event_level_report, expected_aggregatable_report, |
| expected_aggregatable_report)); |
| } |
| |
| TEST_F(AttributionResolverTest, StoreSource_ReturnsMinFakeReportTime) { |
| const base::Time now = base::Time::Now(); |
| |
| const struct { |
| attribution_reporting::RandomizedResponse randomized_response; |
| ::testing::Matcher<StoreSourceResult::Result> matches; |
| } kTestCases[] = { |
| { |
| std::nullopt, |
| VariantWith<StoreSourceResult::Success>(_), |
| }, |
| { |
| std::vector<attribution_reporting::FakeEventLevelReport>(), |
| VariantWith<StoreSourceResult::Success>(Field( |
| &StoreSourceResult::Success::min_fake_report_time, std::nullopt)), |
| }, |
| { |
| std::vector<attribution_reporting::FakeEventLevelReport>{ |
| {.trigger_data = 0, .window_index = 0}, |
| {.trigger_data = 0, .window_index = 1}, |
| {.trigger_data = 0, .window_index = 2}}, |
| VariantWith<StoreSourceResult::Success>( |
| Field(&StoreSourceResult::Success::min_fake_report_time, |
| now + base::Days(1))), |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| delegate()->set_randomized_response(test_case.randomized_response); |
| |
| auto result = storage()->StoreSource( |
| SourceBuilder() |
| .SetTriggerSpecs( |
| TriggerSpecs(SourceType::kNavigation, |
| *attribution_reporting::EventReportWindows::Create( |
| base::Days(0), |
| {base::Days(1), base::Days(2), base::Days(3)}), |
| MaxEventLevelReports::Max())) |
| .Build()); |
| |
| EXPECT_THAT(result.result(), test_case.matches); |
| } |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerPriority) { |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceEventId(3) |
| .SetPriority(0) |
| .SetMaxEventLevelReports(1) |
| .Build()); |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceEventId(5) |
| .SetPriority(1) |
| .SetMaxEventLevelReports(1) |
| .Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder().SetPriority(0).SetDebugKey(20).Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| ReplacedEventLevelReportIs(IsNull()), |
| CreateReportSourceIs(Optional(SourceEventIdIs(5u))), |
| DroppedEventLevelReportIs(IsNull()))); |
| |
| // This conversion should replace the one above because it has a higher |
| // priority. |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder().SetPriority(2).SetDebugKey(21).Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult:: |
| kSuccessDroppedLowerPriority), |
| ReplacedEventLevelReportIs(Pointee(TriggerDebugKeyIs(20u))), |
| CreateReportSourceIs(Optional(SourceEventIdIs(5u))), |
| DroppedEventLevelReportIs(IsNull()))); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceEventId(7) |
| .SetPriority(2) |
| .SetMaxEventLevelReports(1) |
| .Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(1).SetDebugKey(22).Build())); |
| // This conversion should be dropped because it has a lower priority than the |
| // one above. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder().SetPriority(0).SetDebugKey(23).Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kPriorityTooLow), |
| ReplacedEventLevelReportIs(IsNull()), |
| CreateReportSourceIs(Optional(SourceEventIdIs(7u))), |
| DroppedEventLevelReportIs(Pointee(TriggerDebugKeyIs(23u))))); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre( |
| AllOf(EventLevelDataIs(Field( |
| &AttributionReport::EventLevelData::source_event_id, 5u)), |
| TriggerDebugKeyIs(21u)), |
| AllOf(EventLevelDataIs(Field( |
| &AttributionReport::EventLevelData::source_event_id, 7u)), |
| TriggerDebugKeyIs(22u)))); |
| } |
| |
| // Regression test for erroneous use of report_time instead of |
| // initial_report_time in event-level prioritization (http://crbug.com/1500598). |
| TEST_F(AttributionResolverTest, TriggerPriority_UsesOriginalReportTime) { |
| delegate()->use_realistic_report_times(); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetTriggerSpecs( |
| TriggerSpecs(SourceType::kNavigation, |
| *attribution_reporting::EventReportWindows::Create( |
| /*start_time=*/base::Seconds(0), |
| /*end_times=*/ |
| { |
| base::Hours(1), |
| base::Hours(1) + base::Minutes(5), |
| }), |
| MaxEventLevelReports(1))) |
| .Build()); |
| |
| ASSERT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(0).Build())); |
| |
| // Force the second trigger to fall into the second report window. |
| task_environment_.FastForwardBy(base::Hours(1)); |
| |
| const base::Time expected_first_report_time = |
| base::Time::Now() + base::Minutes(5); |
| |
| // Simulate the first report being sent but experiencing a transient failure, |
| // resulting in its report time being adjusted so that it happens to be equal |
| // to that of the second trigger. |
| { |
| std::vector<AttributionReport> reports = |
| storage()->GetAttributionReports(base::Time::Max()); |
| ASSERT_EQ(reports.size(), 1u); |
| |
| ASSERT_TRUE(storage()->UpdateReportForSendFailure( |
| reports.front().id(), expected_first_report_time)); |
| |
| reports = storage()->GetAttributionReports(base::Time::Max()); |
| ASSERT_EQ(reports.size(), 1u); |
| ASSERT_EQ(reports.front().report_time(), expected_first_report_time); |
| } |
| |
| // This one should not replace the previous one despite having a higher |
| // priority because its original report time does not match that of the |
| // previous one. Prior to the fix, this would have returned |
| // `AttributionTrigger::EventLevelResult::kSuccessDroppedLowerPriority`. |
| ASSERT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder().SetPriority(1).Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kExcessiveReports), |
| DroppedEventLevelReportIs( |
| Pointee(ReportTimeIs(expected_first_report_time))))); |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerPriority_Simple) { |
| storage()->StoreSource(SourceBuilder().SetMaxEventLevelReports(1).Build()); |
| |
| int i = 0; |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(i).SetDebugKey(i).Build())); |
| i++; |
| |
| for (; i < 10; i++) { |
| EXPECT_EQ( |
| AttributionTrigger::EventLevelResult::kSuccessDroppedLowerPriority, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(i).SetDebugKey(i).Build())); |
| } |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(TriggerDebugKeyIs(9u))); |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerPriority_SamePriorityDeletesMostRecent) { |
| storage()->StoreSource(SourceBuilder().SetMaxEventLevelReports(2).Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(1).SetTriggerData(3).Build())); |
| |
| // Note: Fast-forwards aren't necessary here because trigger order for the |
| // purposes of prioritization is based on rowid, not trigger time. |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(1).SetTriggerData(2).Build())); |
| |
| // This report should not be stored, as even though it has the same priority |
| // as the previous two, it is the most recent. |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kPriorityTooLow, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(1).SetTriggerData(8).Build())); |
| |
| // This report should be stored by replacing the one with `trigger_data == |
| // 2`, which is the most recent of the two with `priority == 1`. |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccessDroppedLowerPriority, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(2).SetTriggerData(5).Build())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs(TriggerDataIs(3u)), |
| EventLevelDataIs(TriggerDataIs(5u)))); |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerPriority_DeactivatesImpression) { |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceEventId(3) |
| .SetPriority(0) |
| .SetMaxEventLevelReports(1) |
| .Build()); |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceEventId(5) |
| .SetPriority(1) |
| .SetMaxEventLevelReports(1) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(2)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| // Because the impression with data 5 has the highest priority, it is selected |
| // for attribution. The unselected impression with data 3 should be |
| // deactivated, but the one with data 5 should remain active. |
| EXPECT_THAT(storage()->GetActiveSources(), ElementsAre(SourceEventIdIs(5u))); |
| |
| // Ensure that the next report is in a different window. |
| delegate()->set_report_delay(kReportDelay + base::Milliseconds(1)); |
| |
| // This conversion should not be stored because all reports for the attributed |
| // impression were in an earlier window. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder().SetPriority(2).Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kExcessiveReports), |
| DroppedEventLevelReportIs( |
| Pointee(EventLevelDataIs(TriggerPriorityIs(2)))))); |
| |
| // As a result, the impression with data 5 should have reached event-level |
| // attribution limit. |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| ElementsAre(SourceActiveStateIs( |
| StoredSource::ActiveState::kReachedEventLevelAttributionLimit))); |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerPriority_AttributionRateLimitAdjusted) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = base::TimeDelta::Max(); |
| r.max_source_registration_reporting_origins = |
| std::numeric_limits<int64_t>::max(); |
| r.max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(); |
| r.max_attributions = 2; |
| return r; |
| }()); |
| |
| storage()->StoreSource(SourceBuilder().SetMaxEventLevelReports(1).Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(0).SetDebugKey(0).Build())); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccessDroppedLowerPriority, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(1).SetDebugKey(1).Build())); |
| |
| storage()->StoreSource(SourceBuilder().SetMaxEventLevelReports(1).Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetDebugKey(2).Build())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(TriggerDebugKeyIs(1u), TriggerDebugKeyIs(2u))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| TriggerPriority_ReplacementSkipAttributionRateLimitCheck) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.max_attributions = 1; |
| return r; |
| }()); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetTriggerSpecs(TriggerSpecs( |
| SourceType::kNavigation, |
| attribution_reporting::EventReportWindows(), |
| MaxEventLevelReports(1))) |
| .Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(0).Build())); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccessDroppedLowerPriority, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(1).Build())); |
| |
| storage()->StoreSource(SourceBuilder().Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kExcessiveAttributions, |
| MaybeCreateAndStoreEventLevelReport(TriggerBuilder().Build())); |
| } |
| |
| TEST_F(AttributionResolverTest, DedupKey_Dedups) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example")}) |
| .Build()); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.example")}) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(DedupKeysAre(IsEmpty()), DedupKeysAre(IsEmpty()))); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetDedupKey(11) |
| .SetTriggerData(1) |
| .Build())); |
| |
| // Should be stored because dedup key doesn't match even though conversion |
| // destination does. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetDedupKey(12) |
| .SetTriggerData(2) |
| .Build())); |
| |
| // Should be stored because conversion destination doesn't match even though |
| // dedup key does. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://b.example")) |
| .SetDedupKey(12) |
| .SetTriggerData(3) |
| .Build())); |
| |
| // Shouldn't be stored because conversion destination and dedup key match. |
| auto result = storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetDedupKey(11) |
| .SetTriggerData(4) |
| .Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kDeduplicated, |
| result.event_level_status()); |
| EXPECT_FALSE(result.replaced_event_level_report()); |
| |
| // Shouldn't be stored because conversion destination and dedup key match. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kDeduplicated, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://b.example")) |
| .SetDedupKey(12) |
| .SetTriggerData(5) |
| .Build())); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), |
| ElementsAre(EventLevelDataIs(TriggerDataIs(1u)), |
| EventLevelDataIs(TriggerDataIs(2u)), |
| EventLevelDataIs(TriggerDataIs(3u)))); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(DedupKeysAre(ElementsAre(11, 12)), |
| DedupKeysAre(ElementsAre(12)))); |
| } |
| |
| TEST_F(AttributionResolverTest, DedupKey_DedupsAfterConversionDeletion) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example")}) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetDedupKey(2) |
| .SetTriggerData(3) |
| .Build())); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| std::vector<AttributionReport> actual_reports = |
| storage()->GetAttributionReports(base::Time::Now()); |
| EXPECT_THAT(actual_reports, ElementsAre(EventLevelDataIs(TriggerDataIs(3u)))); |
| |
| // Simulate the report being sent and deleted from storage. |
| DeleteReports(actual_reports); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| // This report shouldn't be stored, as it should be deduped against the |
| // previously stored one even though that previous one is no longer in the DB. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kDeduplicated, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetDedupKey(2) |
| .SetTriggerData(5) |
| .Build())); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, AggregatableDedupKey_Dedups) { |
| TestAggregatableSourceProvider provider; |
| storage()->StoreSource( |
| provider.GetBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example")}) |
| .Build()); |
| storage()->StoreSource( |
| provider.GetBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.example")}) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(AggregatableDedupKeysAre(IsEmpty()), |
| AggregatableDedupKeysAre(IsEmpty()))); |
| |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| MaybeCreateAndStoreAggregatableReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetAggregatableDedupKey(11) |
| .SetDebugKey(71) |
| .Build(/*generate_event_trigger_data=*/false))); |
| |
| // Should be stored because dedup key doesn't match even though attribution |
| // destination does. |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| MaybeCreateAndStoreAggregatableReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetAggregatableDedupKey(12) |
| .SetDebugKey(72) |
| .Build(/*generate_event_trigger_data=*/false))); |
| |
| // Should be stored because attribution destination doesn't match even though |
| // dedup key does. |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| MaybeCreateAndStoreAggregatableReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://b.example")) |
| .SetAggregatableDedupKey(12) |
| .SetDebugKey(73) |
| .Build(/*generate_event_trigger_data=*/false))); |
| |
| // Shouldn't be stored because attribution destination and dedup key match. |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kDeduplicated, |
| MaybeCreateAndStoreAggregatableReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetAggregatableDedupKey(11) |
| .SetDebugKey(74) |
| .Build(/*generate_event_trigger_data=*/false))); |
| |
| // Shouldn't be stored because attribution destination and dedup key match. |
| // Note that we intentionally don't set aggregatable values to verify that |
| // the deduplication occurs before aggregatable contributions creation. |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kDeduplicated, |
| MaybeCreateAndStoreAggregatableReport( |
| TriggerBuilder() |
| .SetAggregatableTriggerData( |
| {attribution_reporting::AggregatableTriggerData()}) |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://b.example")) |
| .SetAggregatableDedupKey(12) |
| .SetDebugKey(75) |
| .Build(/*generate_event_trigger_data=*/false))); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), |
| ElementsAre(TriggerDebugKeyIs(71u), TriggerDebugKeyIs(72u), |
| TriggerDebugKeyIs(73u))); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(AggregatableDedupKeysAre(ElementsAre(11, 12)), |
| AggregatableDedupKeysAre(ElementsAre(12)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AggregatableDedupKey_DedupsAfterConversionDeletion) { |
| storage()->StoreSource( |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetSourceEventId(1) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example")}) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| MaybeCreateAndStoreAggregatableReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetAggregatableDedupKey(2) |
| .SetDebugKey(3) |
| .Build(/*generate_event_trigger_data=*/false))); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| std::vector<AttributionReport> actual_reports = |
| storage()->GetAttributionReports(base::Time::Now()); |
| EXPECT_THAT(actual_reports, ElementsAre(TriggerDebugKeyIs(3u))); |
| |
| // Simulate the report being sent and deleted from storage. |
| DeleteReports(actual_reports); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| // This report shouldn't be stored, as it should be deduped against the |
| // previously stored one even though that previous one is no longer in the DB. |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kDeduplicated, |
| MaybeCreateAndStoreAggregatableReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetAggregatableDedupKey(2) |
| .SetDebugKey(5) |
| .Build(/*generate_event_trigger_data=*/false))); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, DedupKey_AggregatableReportNotDedups) { |
| storage()->StoreSource( |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetSourceEventId(1) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example")}) |
| .Build()); |
| |
| auto result = storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetDedupKey(11) |
| .Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| result.event_level_status()); |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| result.aggregatable_status()); |
| |
| result = storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetDedupKey(11) |
| .Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kDeduplicated, |
| result.event_level_status()); |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| result.aggregatable_status()); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AggregatableDedupKey_EventLevelReportNotDedups) { |
| storage()->StoreSource( |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetSourceEventId(1) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.example")}) |
| .Build()); |
| |
| auto result = storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetAggregatableDedupKey(11) |
| .Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| result.event_level_status()); |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| result.aggregatable_status()); |
| |
| result = storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.example")) |
| .SetAggregatableDedupKey(11) |
| .Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| result.event_level_status()); |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kDeduplicated, |
| result.aggregatable_status()); |
| } |
| |
| TEST_F(AttributionResolverTest, AggregatableDedupKeysFiltering) { |
| const auto origin = *SuitableOrigin::Deserialize("https://r.test"); |
| |
| std::vector<attribution_reporting::AggregatableTriggerData> |
| aggregatable_trigger_data{attribution_reporting::AggregatableTriggerData( |
| absl::MakeUint128(/*high=*/1, /*low=*/0), |
| /*source_keys=*/{"0"}, FilterPair())}; |
| |
| auto aggregatable_values = {*AggregatableValues::Create( |
| {{"0", *AggregatableValuesValue::Create(1, kDefaultFilteringId)}}, |
| FilterPair())}; |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites({net::SchemefulSite(origin)}) |
| .SetReportingOrigin(origin) |
| .SetFilterData(*FilterData::Create({{"abc", {"123"}}})) |
| .SetAggregationKeys( |
| *attribution_reporting::AggregationKeys::FromKeys({{"0", 1}})) |
| .Build()); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| AttributionTrigger trigger1( |
| /*reporting_origin=*/origin, attribution_reporting::TriggerRegistration(), |
| /*destination_origin=*/origin, |
| /*is_within_fenced_frame=*/false); |
| |
| trigger1.registration().aggregatable_dedup_keys.emplace_back( |
| /*dedup_key=*/123, FilterPair()); |
| trigger1.registration().aggregatable_trigger_data = aggregatable_trigger_data; |
| trigger1.registration().aggregatable_values = aggregatable_values; |
| |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| MaybeCreateAndStoreAggregatableReport(trigger1)); |
| |
| const struct { |
| const char* desc; |
| attribution_reporting::AggregatableDedupKey aggregatable_dedup_key; |
| bool expectDeduplicated; |
| } kTestCases[] = { |
| { |
| "filter mismatch", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/123, |
| FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"456"}}, |
| })}, |
| /*negative=*/{})), |
| false, |
| }, |
| { |
| "filter match", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/123, |
| FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"123"}}, |
| })}, |
| /*negative=*/{})), |
| true, |
| }, |
| { |
| "filter match wih lookback_window", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/123, FilterPair( |
| /*positive=*/{*FilterConfig::Create( |
| { |
| {"abc", {"123"}}, |
| }, |
| /*lookback_window=*/kReportDelay)}, |
| /*negative=*/{})), |
| true, |
| }, |
| { |
| "filter mismatch due to lookback_window", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/123, FilterPair( |
| /*positive=*/{*FilterConfig::Create( |
| |
| { |
| {"abc", {"123"}}, |
| }, |
| /*lookback_window=*/kReportDelay - |
| base::Microseconds(1))}, |
| /*negative=*/{})), |
| false, |
| }, |
| { |
| "negated filters false", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/123, |
| FilterPair( |
| /*positive=*/{}, |
| /*negative=*/attribution_reporting::FiltersForSourceType( |
| SourceType::kNavigation))), |
| false, |
| }, |
| { |
| "negated filters false due to lookback_window", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/123, |
| FilterPair( |
| /*positive=*/{}, |
| /*negative=*/attribution_reporting::FiltersForSourceType( |
| SourceType::kEvent, |
| /*lookback_window=*/kReportDelay))), |
| false, |
| }, |
| { |
| "negated filters true", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/123, |
| FilterPair( |
| /*positive=*/{}, |
| /*negative=*/attribution_reporting::FiltersForSourceType( |
| SourceType::kEvent))), |
| true, |
| }, |
| { |
| "negated filters true with lookback_window", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/123, |
| FilterPair( |
| /*positive=*/{}, |
| /*negative=*/attribution_reporting::FiltersForSourceType( |
| SourceType::kEvent, |
| /*lookback_window=*/kReportDelay - |
| base::Microseconds(1)))), |
| true, |
| }, |
| { |
| "null dedup key", |
| attribution_reporting::AggregatableDedupKey( |
| /*dedup_key=*/std::nullopt, |
| FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"123"}}, |
| })}, |
| /*negative=*/{})), |
| false, |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| AttributionTrigger trigger2( |
| /*reporting_origin=*/origin, |
| attribution_reporting::TriggerRegistration(), |
| /*destination_origin=*/origin, |
| /*is_within_fenced_frame=*/false); |
| |
| trigger2.registration().aggregatable_dedup_keys.emplace_back( |
| test_case.aggregatable_dedup_key); |
| trigger2.registration().aggregatable_trigger_data = |
| aggregatable_trigger_data; |
| trigger2.registration().aggregatable_values = aggregatable_values; |
| |
| EXPECT_EQ(MaybeCreateAndStoreAggregatableReport(trigger2), |
| test_case.expectDeduplicated |
| ? AttributionTrigger::AggregatableResult::kDeduplicated |
| : AttributionTrigger::AggregatableResult::kSuccess) |
| << test_case.desc; |
| } |
| } |
| |
| TEST_F(AttributionResolverTest, GetAttributionReports_SetsPriority) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetPriority(13).Build())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs(TriggerPriorityIs(13)))); |
| } |
| |
| TEST_F(AttributionResolverTest, NoIDReuse_Impression) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| auto sources = storage()->GetActiveSources(); |
| const StoredSource::Id id1 = sources.front().source_id(); |
| |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| EXPECT_THAT(storage()->GetActiveSources(), IsEmpty()); |
| |
| storage()->StoreSource(SourceBuilder().Build()); |
| sources = storage()->GetActiveSources(); |
| const StoredSource::Id id2 = sources.front().source_id(); |
| |
| EXPECT_NE(id1, id2); |
| } |
| |
| TEST_F(AttributionResolverTest, NoIDReuse_Conversion) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| auto reports = storage()->GetAttributionReports(base::Time::Max()); |
| ASSERT_THAT(reports, SizeIs(1)); |
| const AttributionReport::Id id1 = reports.front().id(); |
| |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| reports = storage()->GetAttributionReports(base::Time::Max()); |
| ASSERT_THAT(reports, SizeIs(1)); |
| const AttributionReport::Id id2 = reports.front().id(); |
| |
| EXPECT_NE(id1, id2); |
| } |
| |
| TEST_F(AttributionResolverTest, UpdateReportForSendFailure) { |
| storage()->StoreSource(TestAggregatableSourceProvider().GetBuilder().Build()); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| std::vector<AttributionReport> actual_reports = |
| storage()->GetAttributionReports(base::Time::Now()); |
| EXPECT_THAT( |
| actual_reports, |
| ElementsAre( |
| AllOf(ReportTypeIs(AttributionReport::Type::kEventLevel), |
| FailedSendAttemptsIs(0)), |
| AllOf(ReportTypeIs(AttributionReport::Type::kAggregatableAttribution), |
| FailedSendAttemptsIs(0)))); |
| |
| const base::TimeDelta delay = base::Days(2); |
| const base::Time new_report_time = actual_reports[0].report_time() + delay; |
| EXPECT_TRUE(storage()->UpdateReportForSendFailure(actual_reports[0].id(), |
| new_report_time)); |
| EXPECT_TRUE(storage()->UpdateReportForSendFailure(actual_reports[1].id(), |
| new_report_time)); |
| |
| task_environment_.FastForwardBy(delay); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Now()), |
| ElementsAre( |
| AllOf(FailedSendAttemptsIs(1), ReportTimeIs(new_report_time)), |
| AllOf(FailedSendAttemptsIs(1), ReportTimeIs(new_report_time)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaybeCreateAndStoreEventLevelReport_ReturnsDeactivatedSources) { |
| storage()->StoreSource( |
| SourceBuilder().SetMaxEventLevelReports(kMaxConversions).Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| |
| // Store the maximum number of reports for the source. |
| for (size_t i = 1; i <= kMaxConversions; i++) { |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| } |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| auto reports = storage()->GetAttributionReports(base::Time::Now()); |
| EXPECT_THAT(reports, SizeIs(3)); |
| |
| // Simulate the reports being sent and removed from storage. |
| DeleteReports(reports); |
| |
| // The next trigger should cause the source to reach event-level attribution |
| // limit; the report itself shouldn't be stored as we've already reached the |
| // maximum number of event-level reports per source. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder().SetDebugKey(20).Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kExcessiveReports), |
| ReplacedEventLevelReportIs(IsNull()), |
| DroppedEventLevelReportIs(Pointee(TriggerDebugKeyIs(20u))))); |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| ElementsAre(SourceActiveStateIs( |
| StoredSource::ActiveState::kReachedEventLevelAttributionLimit))); |
| } |
| |
| TEST_F(AttributionResolverTest, ReportID_RoundTrips) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| std::vector<AttributionReport> actual_reports = |
| storage()->GetAttributionReports(base::Time::Max()); |
| EXPECT_EQ(1u, actual_reports.size()); |
| EXPECT_EQ(DefaultExternalReportID(), actual_reports[0].external_report_id()); |
| } |
| |
| TEST_F(AttributionResolverTest, AdjustOfflineReportTimes) { |
| EXPECT_EQ(storage()->AdjustOfflineReportTimes(), std::nullopt); |
| |
| delegate()->set_offline_report_delay_config( |
| AttributionResolverDelegate::OfflineReportDelayConfig{ |
| .min = base::Hours(1), .max = base::Hours(1)}); |
| EXPECT_EQ(storage()->AdjustOfflineReportTimes(), std::nullopt); |
| |
| storage()->StoreSource(TestAggregatableSourceProvider().GetBuilder().Build()); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| const base::Time original_report_time = base::Time::Now() + kReportDelay; |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(ReportTimeIs(original_report_time), |
| AllOf(ReportTimeIs(original_report_time), |
| InitialReportTimeIs(original_report_time)))); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| EXPECT_EQ(storage()->AdjustOfflineReportTimes(), original_report_time); |
| |
| // The report time should not be changed as it is equal to now, not strictly |
| // less than it. |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(ReportTimeIs(original_report_time), |
| AllOf(ReportTimeIs(original_report_time), |
| InitialReportTimeIs(original_report_time)))); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| const base::Time new_report_time = base::Time::Now() + base::Hours(1); |
| |
| EXPECT_EQ(storage()->AdjustOfflineReportTimes(), new_report_time); |
| |
| // The report time should be changed as it is strictly less than now. |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(ReportTimeIs(new_report_time), |
| AllOf(ReportTimeIs(new_report_time), |
| InitialReportTimeIs(original_report_time)))); |
| } |
| |
| TEST_F(AttributionResolverTest, AdjustOfflineReportTimes_Range) { |
| delegate()->set_offline_report_delay_config( |
| AttributionResolverDelegate::OfflineReportDelayConfig{ |
| .min = base::Hours(1), .max = base::Hours(3)}); |
| |
| storage()->StoreSource(TestAggregatableSourceProvider().GetBuilder().Build()); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| const base::Time original_report_time = base::Time::Now() + kReportDelay; |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(ReportTimeIs(original_report_time), |
| AllOf(ReportTimeIs(original_report_time), |
| InitialReportTimeIs(original_report_time)))); |
| |
| task_environment_.FastForwardBy(kReportDelay + base::Milliseconds(1)); |
| |
| storage()->AdjustOfflineReportTimes(); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre( |
| ReportTimeIs(AllOf(Ge(base::Time::Now() + base::Hours(1)), |
| Le(base::Time::Now() + base::Hours(3)))), |
| AllOf(ReportTimeIs(AllOf(Ge(base::Time::Now() + base::Hours(1)), |
| Le(base::Time::Now() + base::Hours(3)))), |
| InitialReportTimeIs(original_report_time)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AdjustOfflineReportTimes_ReturnsMinReportTimeWithoutDelay) { |
| delegate()->set_offline_report_delay_config(std::nullopt); |
| |
| ASSERT_EQ(storage()->AdjustOfflineReportTimes(), std::nullopt); |
| |
| storage()->StoreSource(SourceBuilder().Build()); |
| ASSERT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| std::vector<AttributionReport> reports = |
| storage()->GetAttributionReports(base::Time::Max()); |
| ASSERT_THAT(reports, SizeIs(1)); |
| |
| ASSERT_EQ(storage()->AdjustOfflineReportTimes(), |
| reports.front().report_time()); |
| } |
| |
| TEST_F(AttributionResolverTest, GetNextEventReportTime) { |
| const auto origin_a = *SuitableOrigin::Deserialize("https://a.example/"); |
| const auto origin_b = *SuitableOrigin::Deserialize("https://b.example/"); |
| |
| EXPECT_EQ(storage()->GetNextReportTime(base::Time::Min()), std::nullopt); |
| |
| storage()->StoreSource(SourceBuilder().SetReportingOrigin(origin_a).Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetReportingOrigin(origin_a).Build())); |
| |
| const base::Time report_time_a = base::Time::Now() + kReportDelay; |
| |
| EXPECT_EQ(storage()->GetNextReportTime(base::Time::Min()), report_time_a); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_a), std::nullopt); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| storage()->StoreSource(SourceBuilder().SetReportingOrigin(origin_b).Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetReportingOrigin(origin_b).Build())); |
| |
| const base::Time report_time_b = base::Time::Now() + kReportDelay; |
| |
| EXPECT_EQ(storage()->GetNextReportTime(base::Time::Min()), report_time_a); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_a), report_time_b); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_b), std::nullopt); |
| } |
| |
| TEST_F(AttributionResolverTest, GetAttributionReports_Shuffles) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetTriggerData(3).Build())); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetTriggerData(1).Build())); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetTriggerData(2).Build())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports( |
| /*max_report_time=*/base::Time::Max(), /*limit=*/-1), |
| ElementsAre(EventLevelDataIs(TriggerDataIs(3)), |
| EventLevelDataIs(TriggerDataIs(1)), |
| EventLevelDataIs(TriggerDataIs(2)))); |
| |
| delegate()->set_reverse_reports_on_shuffle(true); |
| |
| EXPECT_THAT(storage()->GetAttributionReports( |
| /*max_report_time=*/base::Time::Max(), /*limit=*/-1), |
| ElementsAre(EventLevelDataIs(TriggerDataIs(2)), |
| EventLevelDataIs(TriggerDataIs(1)), |
| EventLevelDataIs(TriggerDataIs(3)))); |
| } |
| |
| TEST_F(AttributionResolverTest, GetAttributionReportsExceedLimit_Shuffles) { |
| storage()->StoreSource(TestAggregatableSourceProvider().GetBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetTriggerData(3).Build())); |
| |
| delegate()->set_report_delay(base::Hours(1)); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetTriggerData(1).Build())); |
| |
| delegate()->set_report_delay(base::Hours(2)); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetTriggerData(2).Build())); |
| |
| // Will be dropped as the report time is latest. |
| delegate()->set_report_delay(base::Hours(3)); |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| MaybeCreateAndStoreAggregatableReport( |
| DefaultAggregatableTriggerBuilder().Build())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports( |
| /*max_report_time=*/base::Time::Max(), /*limit=*/3), |
| ElementsAre(EventLevelDataIs(TriggerDataIs(3)), |
| EventLevelDataIs(TriggerDataIs(1)), |
| EventLevelDataIs(TriggerDataIs(2)))); |
| |
| delegate()->set_reverse_reports_on_shuffle(true); |
| |
| EXPECT_THAT(storage()->GetAttributionReports( |
| /*max_report_time=*/base::Time::Max(), /*limit=*/3), |
| ElementsAre(EventLevelDataIs(TriggerDataIs(2)), |
| EventLevelDataIs(TriggerDataIs(1)), |
| EventLevelDataIs(TriggerDataIs(3)))); |
| } |
| |
| TEST_F(AttributionResolverTest, GetAttributionDataKeysSet) { |
| auto expected_1 = AttributionDataModel::DataKey( |
| url::Origin::Create(GURL("https://a.r.test"))); |
| auto expected_2 = AttributionDataModel::DataKey( |
| url::Origin::Create(GURL("https://b.r.test"))); |
| auto expected_3 = AttributionDataModel::DataKey( |
| url::Origin::Create(GURL("https://c.r.test"))); |
| |
| auto s1 = |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://a.r.test")) |
| .Build(); |
| auto s2 = |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://b.r.test")) |
| .SetSourceOrigin(*SuitableOrigin::Deserialize("https://s1.test")) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .Build(); |
| auto s3 = |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://b.r.test")) |
| .SetSourceOrigin(*SuitableOrigin::Deserialize("https://s2.test")) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d2.test")}) |
| .Build(); |
| |
| storage()->StoreSource(s1); |
| storage()->StoreSource(s1); |
| storage()->StoreSource(s2); |
| storage()->StoreSource(s3); |
| |
| storage()->ProcessAggregatableDebugReport( |
| CreateAggregatableDebugReport({AggregatableReportHistogramContribution( |
| /*bucket=*/1, /*value=*/1, |
| /*filtering_id=*/std::nullopt)}, |
| /*reporting_origin=*/"https://c.r.test"), |
| /*remaining_budget=*/std::nullopt, |
| /*source_id=*/std::nullopt); |
| |
| EXPECT_THAT(storage()->GetAllDataKeys(), |
| ElementsAre(expected_1, expected_2, expected_3)); |
| } |
| |
| TEST_F(AttributionResolverTest, SourceDebugKey_RoundTrips) { |
| storage()->StoreSource( |
| SourceBuilder().SetDebugKey(33).SetCookieBasedDebugAllowed(true).Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), ElementsAre(SourceDebugKeyIs(33))); |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerDebugKey_RoundTrips) { |
| storage()->StoreSource( |
| SourceBuilder().SetDebugKey(22).SetCookieBasedDebugAllowed(true).Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetDebugKey(33).Build())); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(AllOf(ReportSourceDebugKeyIs(22), TriggerDebugKeyIs(33)))); |
| } |
| |
| TEST_F(AttributionResolverTest, AttributionAggregationKeys_RoundTrips) { |
| auto aggregation_keys = |
| attribution_reporting::AggregationKeys::FromKeys({{"key", 345}}); |
| ASSERT_TRUE(aggregation_keys.has_value()); |
| storage()->StoreSource( |
| SourceBuilder().SetAggregationKeys(*aggregation_keys).Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(AggregationKeysAre(*aggregation_keys))); |
| } |
| |
| TEST_F(AttributionResolverTest, MaybeCreateAndStoreReport_ReturnsNewReport) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(TriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| NewEventLevelReportIs(Pointee(EventLevelDataIs(_))), |
| NewAggregatableReportIs(IsNull()))); |
| } |
| |
| // This is tested more thoroughly by the `RateLimitTable` unit tests. Here just |
| // ensure that the rate limits are consulted at all. |
| TEST_F(AttributionResolverTest, MaxReportingOriginsPerSource) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = base::TimeDelta::Max(); |
| r.max_source_registration_reporting_origins = 2; |
| r.max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(); |
| r.max_attributions = std::numeric_limits<int64_t>::max(); |
| return r; |
| }()); |
| |
| auto result = storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r1.test")) |
| .SetDebugKey(1) |
| .SetCookieBasedDebugAllowed(true) |
| .Build()); |
| ASSERT_EQ(result.status(), StorableSource::Result::kSuccess); |
| |
| result = storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r2.test")) |
| .SetDebugKey(2) |
| .SetCookieBasedDebugAllowed(true) |
| .Build()); |
| ASSERT_EQ(result.status(), StorableSource::Result::kSuccess); |
| |
| delegate()->set_randomized_response( |
| std::vector<attribution_reporting::FakeEventLevelReport>{}); |
| result = storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r3.test")) |
| .SetDebugKey(3) |
| .SetCookieBasedDebugAllowed(true) |
| .Build()); |
| delegate()->set_randomized_response(std::nullopt); |
| ASSERT_EQ(result.status(), |
| StorableSource::Result::kExcessiveReportingOrigins); |
| EXPECT_TRUE(result.is_noised()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceDebugKeyIs(1), SourceDebugKeyIs(2))); |
| } |
| |
| // This is tested more thoroughly by the `RateLimitTable` unit tests. Here just |
| // ensure that the rate limits are consulted at all and the rate limit is shared |
| // between event-level and aggregatable reports. |
| TEST_F(AttributionResolverTest, MaxReportingOriginsPerAttribution) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = base::TimeDelta::Max(); |
| r.max_source_registration_reporting_origins = |
| std::numeric_limits<int64_t>::max(); |
| r.max_attribution_reporting_origins = 2; |
| r.max_attributions = std::numeric_limits<int64_t>::max(); |
| return r; |
| }()); |
| |
| const auto origin1 = *SuitableOrigin::Deserialize("https://r1.test"); |
| const auto origin2 = *SuitableOrigin::Deserialize("https://r2.test"); |
| const auto origin3 = *SuitableOrigin::Deserialize("https://r3.test"); |
| |
| SourceBuilder source_builder = TestAggregatableSourceProvider().GetBuilder(); |
| TriggerBuilder aggregatable_trigger_builder = |
| DefaultAggregatableTriggerBuilder(); |
| |
| storage()->StoreSource(source_builder.SetReportingOrigin(origin1).Build()); |
| storage()->StoreSource(source_builder.SetReportingOrigin(origin2).Build()); |
| storage()->StoreSource(source_builder.SetReportingOrigin(origin3).Build()); |
| ASSERT_THAT(storage()->GetActiveSources(), SizeIs(3)); |
| |
| ASSERT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder().SetReportingOrigin(origin1).SetDebugKey(1).Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kNotRegistered))); |
| |
| ASSERT_THAT(storage()->MaybeCreateAndStoreReport( |
| aggregatable_trigger_builder.SetReportingOrigin(origin2) |
| .SetDebugKey(2) |
| .Build(/*generate_event_trigger_data=*/false)), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNotRegistered), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| ASSERT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| aggregatable_trigger_builder.SetReportingOrigin(origin3) |
| .SetDebugKey(3) |
| .Build()), |
| AllOf( |
| Property( |
| &CreateReportResult::event_level_result, |
| VariantWith<CreateReportResult::ExcessiveReportingOrigins>(Field( |
| &CreateReportResult::ExcessiveReportingOrigins::max, 2))), |
| Property( |
| &CreateReportResult::aggregatable_result, |
| VariantWith<CreateReportResult::ExcessiveReportingOrigins>(Field( |
| &CreateReportResult::ExcessiveReportingOrigins::max, 2))))); |
| |
| // One event-level report, one aggregatable report. |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre(TriggerDebugKeyIs(1), TriggerDebugKeyIs(2))); |
| } |
| |
| TEST_F(AttributionResolverTest, SourceBudgetValueRetrieved) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(RemainingAggregatableAttributionBudgetIs(65536))); |
| } |
| |
| TEST_F(AttributionResolverTest, MaxAggregatableBudgetPerSource) { |
| auto provider = TestAggregatableSourceProvider(/*size=*/2); |
| storage()->StoreSource(provider.GetBuilder().Build()); |
| |
| // Note: A single contribution can't exceed the budget because |
| // `AggregatableValues::Create()`, which is used by |
| // `DefaultAggregatableTriggerBuilder()`, prevents such an instance from being |
| // constructed. |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{2, 5}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess)); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{ |
| attribution_reporting::kMaxAggregatableValue - 6}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kInsufficientBudget)); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{ |
| attribution_reporting::kMaxAggregatableValue - 7}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess)); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{1}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kInsufficientBudget)); |
| |
| // The second source has higher priority and should have capacity. |
| storage()->StoreSource(provider.GetBuilder().SetPriority(10).Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{ |
| attribution_reporting::kMaxAggregatableValue}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess)); |
| } |
| |
| TEST_F(AttributionResolverTest, BudgetConsumedAfterTriggerIsRetrieved) { |
| auto provider = TestAggregatableSourceProvider(/*size=*/1); |
| storage()->StoreSource(provider.GetBuilder().Build()); |
| |
| EXPECT_EQ( |
| MaybeCreateAndStoreAggregatableReport(DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{2}) |
| .Build()), |
| AttributionTrigger::AggregatableResult::kSuccess); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(RemainingAggregatableAttributionBudgetIs(65536 - 2))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| GetAttributionReports_SetsRandomizedTriggerRate) { |
| delegate()->set_randomized_response_rate(0.1); |
| |
| const auto origin1 = *SuitableOrigin::Deserialize("https://r1.test"); |
| const auto origin2 = *SuitableOrigin::Deserialize("https://r2.test"); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetReportingOrigin(origin1) |
| .SetSourceType(SourceType::kNavigation) |
| .Build()); |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetReportingOrigin(origin1).Build()); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetReportingOrigin(origin2) |
| .SetSourceType(SourceType::kEvent) |
| .Build()); |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetReportingOrigin(origin2).Build()); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre( |
| EventLevelDataIs(AllOf( |
| Field(&AttributionReport::EventLevelData::source_type, |
| SourceType::kNavigation), |
| Field( |
| &AttributionReport::EventLevelData::randomized_response_rate, |
| 0.1))), |
| EventLevelDataIs(AllOf( |
| Field(&AttributionReport::EventLevelData::source_type, |
| SourceType::kEvent), |
| Field( |
| &AttributionReport::EventLevelData::randomized_response_rate, |
| 0.1))))); |
| } |
| |
| TEST_F(AttributionResolverTest, RandomizedResponseRatePerSourceUsed) { |
| delegate()->set_randomized_response_rate(0.1); |
| storage()->StoreSource(SourceBuilder().Build()); |
| delegate()->set_randomized_response_rate(0.2); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre(EventLevelDataIs(Field( |
| &AttributionReport::EventLevelData::randomized_response_rate, 0.1)))); |
| } |
| |
| // Will return minimum of next event-level report and next aggregatable report |
| // time if both present. |
| TEST_F(AttributionResolverTest, GetNextReportTime) { |
| EXPECT_EQ(storage()->GetNextReportTime(base::Time::Min()), std::nullopt); |
| |
| storage()->StoreSource(TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetMaxEventLevelReports(1) |
| .Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| const base::Time report_time_a = base::Time::Now() + kReportDelay; |
| |
| EXPECT_EQ(storage()->GetNextReportTime(base::Time::Min()), report_time_a); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_a), std::nullopt); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| MaybeCreateAndStoreAggregatableReport( |
| DefaultAggregatableTriggerBuilder().Build())); |
| |
| const base::Time report_time_b = base::Time::Now() + kReportDelay; |
| |
| EXPECT_EQ(storage()->GetNextReportTime(base::Time::Min()), report_time_a); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_a), report_time_b); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_b), std::nullopt); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| storage()->StoreSource(SourceBuilder().SetMaxEventLevelReports(1).Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| base::Time report_time_c = base::Time::Now() + kReportDelay; |
| |
| EXPECT_EQ(storage()->GetNextReportTime(base::Time::Min()), report_time_a); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_a), report_time_b); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_b), report_time_c); |
| EXPECT_EQ(storage()->GetNextReportTime(report_time_c), std::nullopt); |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerDataSanitized) { |
| const auto origin1 = *SuitableOrigin::Deserialize("https://r1.test"); |
| const auto origin2 = *SuitableOrigin::Deserialize("https://r2.test"); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetReportingOrigin(origin1) |
| .SetSourceType(SourceType::kNavigation) |
| .Build()); |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetReportingOrigin(origin1).SetTriggerData(8).Build()); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetReportingOrigin(origin2) |
| .SetSourceType(SourceType::kEvent) |
| .Build()); |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetReportingOrigin(origin2).SetTriggerData(3).Build()); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre( |
| EventLevelDataIs(AllOf( |
| Field(&AttributionReport::EventLevelData::source_type, |
| SourceType::kNavigation), |
| TriggerDataIs(0))), |
| EventLevelDataIs(AllOf( |
| Field(&AttributionReport::EventLevelData::source_type, |
| SourceType::kEvent), |
| TriggerDataIs(1))))); |
| } |
| |
| TEST_F(AttributionResolverTest, SourceFilterData_RoundTrips) { |
| storage()->StoreSource(SourceBuilder() |
| .SetFilterData(FilterData()) |
| .SetSourceType(SourceType::kNavigation) |
| .Build()); |
| |
| const auto filter_data = FilterData::Create({{"abc", {"x", "y"}}}); |
| ASSERT_TRUE(filter_data.has_value()); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetFilterData(*filter_data) |
| .SetSourceType(SourceType::kEvent) |
| .Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceFilterDataIs(FilterData()), |
| SourceFilterDataIs(filter_data))); |
| } |
| |
| TEST_F(AttributionResolverTest, NoMatchingTriggerData_ReturnsError) { |
| const auto origin = *SuitableOrigin::Deserialize("https://r.test"); |
| |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceType(SourceType::kNavigation) |
| .SetDestinationSites({net::SchemefulSite(origin)}) |
| .SetReportingOrigin(origin) |
| .Build()); |
| |
| attribution_reporting::TriggerRegistration registration; |
| registration.event_triggers.emplace_back( |
| /*data=*/11, |
| /*priority=*/12, |
| /*dedup_key=*/13, |
| FilterPair( |
| /*positive=*/attribution_reporting::FiltersForSourceType( |
| SourceType::kEvent), |
| /*negative=*/{})); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kNoMatchingConfigurations, |
| MaybeCreateAndStoreEventLevelReport(AttributionTrigger( |
| /*reporting_origin=*/origin, std::move(registration), |
| /*destination_origin=*/origin, |
| /*is_within_fenced_frame=*/false))); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(DedupKeysAre(IsEmpty()))); |
| } |
| |
| TEST_F(AttributionResolverTest, MatchingTriggerData_UsesCorrectData) { |
| const auto origin = *SuitableOrigin::Deserialize("https://r.test"); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceType(SourceType::kNavigation) |
| .SetDestinationSites({net::SchemefulSite(origin)}) |
| .SetReportingOrigin(origin) |
| .SetFilterData(*FilterData::Create({{"abc", {"123"}}})) |
| .Build()); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| attribution_reporting::TriggerRegistration registration; |
| |
| // Filters don't match. |
| registration.event_triggers.emplace_back( |
| /*data=*/1, |
| /*priority=*/12, |
| /*dedup_key=*/13, |
| FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"456"}}, |
| })}, |
| /*negative=*/{})); |
| |
| // Filters match, but negated filters do not. |
| registration.event_triggers.emplace_back( |
| /*data=*/2, |
| /*priority=*/22, |
| /*dedup_key=*/23, |
| FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"123"}}, |
| })}, |
| /*negative=*/{*FilterConfig::Create({ |
| {"source_type", {"navigation"}}, |
| })})); |
| |
| // Filters and negated filters match. |
| registration.event_triggers.emplace_back( |
| /*data=*/3, |
| /*priority=*/32, |
| /*dedup_key=*/33, |
| FilterPair( |
| /*positive=*/{*FilterConfig::Create({ |
| {"abc", {"123"}}, |
| })}, |
| /*negative=*/{*FilterConfig::Create({{"source_type", {"event"}}})})); |
| |
| // Filters and negated filters match, but not the first event |
| // trigger to match. |
| registration.event_triggers.emplace_back( |
| /*data=*/4, |
| /*priority=*/42, |
| /*dedup_key=*/43, |
| FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"123"}}, |
| })}, |
| /*negative=*/{*FilterConfig::Create({ |
| {"source_type", {"event"}}, |
| })})); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(AttributionTrigger( |
| /*reporting_origin=*/origin, std::move(registration), |
| /*destination_origin=*/origin, |
| /*is_within_fenced_frame=*/false))); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs( |
| AllOf(TriggerDataIs(3), TriggerPriorityIs(32))))); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(DedupKeysAre(ElementsAre(33)))); |
| } |
| |
| TEST_F(AttributionResolverTest, TopLevelTriggerFiltering) { |
| const auto origin = *SuitableOrigin::Deserialize("https://r.test"); |
| |
| std::vector<attribution_reporting::EventTriggerData> event_triggers = { |
| attribution_reporting::EventTriggerData( |
| /*data=*/11, |
| /*priority=*/12, |
| /*dedup_key=*/13, FilterPair())}; |
| |
| std::vector<attribution_reporting::AggregatableTriggerData> |
| aggregatable_trigger_data = { |
| attribution_reporting::AggregatableTriggerData( |
| absl::MakeUint128(/*high=*/1, /*low=*/0), |
| /*source_keys=*/{"0"}, FilterPair())}; |
| |
| auto aggregatable_values = {*AggregatableValues::Create( |
| {{"0", *AggregatableValuesValue::Create(1, kDefaultFilteringId)}}, |
| FilterPair())}; |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites({net::SchemefulSite(origin)}) |
| .SetReportingOrigin(origin) |
| .SetFilterData(*FilterData::Create({{"abc", {"123"}}})) |
| .SetAggregationKeys( |
| *attribution_reporting::AggregationKeys::FromKeys({{"0", 1}})) |
| .Build()); |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| AttributionTrigger trigger1( |
| /*reporting_origin=*/origin, attribution_reporting::TriggerRegistration(), |
| /*destination_origin=*/origin, |
| /*is_within_fenced_frame=*/false); |
| trigger1.registration().filters.positive.emplace_back(*FilterConfig::Create({ |
| {"abc", {"456"}}, |
| })); |
| trigger1.registration().event_triggers = event_triggers; |
| trigger1.registration().aggregatable_trigger_data = aggregatable_trigger_data; |
| trigger1.registration().aggregatable_values = aggregatable_values; |
| |
| AttributionTrigger trigger2( |
| /*reporting_origin=*/origin, attribution_reporting::TriggerRegistration(), |
| /*destination_origin=*/origin, |
| /*is_within_fenced_frame=*/false); |
| trigger2.registration().filters.positive.emplace_back(*FilterConfig::Create( |
| { |
| {"abc", {"123"}}, |
| }, |
| /*lookback_window=*/kReportDelay)); |
| trigger2.registration().event_triggers = event_triggers; |
| trigger2.registration().aggregatable_trigger_data = aggregatable_trigger_data; |
| trigger2.registration().aggregatable_values = aggregatable_values; |
| |
| AttributionTrigger trigger3( |
| /*reporting_origin=*/origin, attribution_reporting::TriggerRegistration(), |
| /*destination_origin=*/origin, |
| /*is_within_fenced_frame=*/false); |
| trigger3.registration().filters.negative = |
| attribution_reporting::FiltersForSourceType(SourceType::kNavigation); |
| trigger3.registration().event_triggers = event_triggers; |
| trigger3.registration().aggregatable_trigger_data = aggregatable_trigger_data; |
| trigger3.registration().aggregatable_values = aggregatable_values; |
| |
| AttributionTrigger trigger4( |
| /*reporting_origin=*/origin, attribution_reporting::TriggerRegistration(), |
| /*destination_origin=*/origin, |
| /*is_within_fenced_frame=*/false); |
| trigger4.registration().filters.positive.emplace_back(*FilterConfig::Create( |
| { |
| {"abc", {"123"}}, |
| }, |
| /*lookback_window=*/kReportDelay - base::Microseconds(1))); |
| trigger4.registration().event_triggers = event_triggers; |
| trigger4.registration().aggregatable_trigger_data = aggregatable_trigger_data; |
| trigger4.registration().aggregatable_values = aggregatable_values; |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(trigger1), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult:: |
| kNoMatchingSourceFilterData), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult:: |
| kNoMatchingSourceFilterData))); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(trigger2), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(trigger3), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult:: |
| kNoMatchingSourceFilterData), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult:: |
| kNoMatchingSourceFilterData))); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(trigger4), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult:: |
| kNoMatchingSourceFilterData), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult:: |
| kNoMatchingSourceFilterData))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AggregatableAttributionNoMatchingSources_NoSourcesReturned) { |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kNoMatchingImpressions), |
| NewEventLevelReportIs(IsNull()), |
| NewAggregatableReportIs(IsNull()))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AggregatableAttributionNoAggregationKeys_NoContributions) { |
| storage()->StoreSource(SourceBuilder().Build()); |
| |
| AttributionTrigger trigger = |
| DefaultAggregatableTriggerBuilder(/*histogram_values=*/{5}) |
| .SetTriggerData(5) |
| .Build(); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(trigger), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kNoHistograms), |
| NewEventLevelReportIs(Pointee(EventLevelDataIs(TriggerDataIs(5)))), |
| NewAggregatableReportIs(IsNull()))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AggregatableAttributionValues_NoMatchingFilters_NoContributions) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetAggregationKeys( |
| *attribution_reporting::AggregationKeys::FromKeys({{"0", 1}})) |
| .SetFilterData( |
| *attribution_reporting::FilterData::Create({{"product", {"1"}}})) |
| .Build()); |
| |
| EXPECT_EQ(MaybeCreateAndStoreAggregatableReport( |
| TriggerBuilder() |
| .SetAggregatableValues({*AggregatableValues::Create( |
| {{"0", *AggregatableValuesValue::Create( |
| 123, kDefaultFilteringId)}}, |
| FilterPair( |
| /*positive=*/{*FilterConfig::Create( |
| {{"product", {"2"}}})}, |
| /*negative=*/{}))}) |
| .Build()), |
| AttributionTrigger::AggregatableResult::kNoHistograms); |
| } |
| |
| TEST_F(AttributionResolverTest, AggregatableAttribution_ReportsScheduled) { |
| auto source_builder = TestAggregatableSourceProvider().GetBuilder(); |
| storage()->StoreSource(source_builder.Build()); |
| |
| AttributionTrigger trigger = |
| DefaultAggregatableTriggerBuilder(/*histogram_values=*/{5}).Build(); |
| auto contributions = |
| DefaultAggregatableHistogramContributions(/*histogram_values=*/{5}); |
| ASSERT_THAT(contributions, SizeIs(1)); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport(trigger), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| NewEventLevelReportIs(Pointee(EventLevelDataIs(_))), |
| NewAggregatableReportIs(Pointee(AggregatableAttributionDataIs( |
| AggregatableHistogramContributionsAre(contributions)))))); |
| |
| const auto source = |
| source_builder.SetRemainingAggregatableAttributionBudget(65536 - 5) |
| .BuildStored(); |
| auto expected_aggregatable_report = |
| GetExpectedAggregatableReport(source, std::move(contributions), trigger); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(EventLevelDataIs(_), expected_aggregatable_report)); |
| |
| EXPECT_EQ(expected_aggregatable_report.report_time(), |
| expected_aggregatable_report.initial_report_time()); |
| } |
| |
| TEST_F( |
| AttributionResolverTest, |
| MaybeCreateAndStoreAggregatableReport_reachedEventLevelAttributionLimit) { |
| storage()->StoreSource(TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetMaxEventLevelReports(kMaxConversions) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| |
| // Store the maximum number of reports for the source. |
| for (size_t i = 1; i <= kMaxConversions; i++) { |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| } |
| |
| task_environment_.FastForwardBy(kReportDelay); |
| auto reports = storage()->GetAttributionReports(base::Time::Now()); |
| // 3 event-level reports, 3 aggregatable reports |
| EXPECT_THAT(reports, SizeIs(6)); |
| |
| // Simulate the reports being sent and removed from storage. |
| DeleteReports(reports); |
| |
| // The next trigger should cause the source to reach event-level attribution |
| // limit; the event-level report itself shouldn't be stored as we've already |
| // reached the maximum number of event-level reports per source, whereas the |
| // aggregatable report is still stored. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder(/*histogram_values=*/{5}) |
| .SetTriggerData(5) |
| .Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kExcessiveReports), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| ReplacedEventLevelReportIs(IsNull()), |
| NewEventLevelReportIs(IsNull()), |
| NewAggregatableReportIs(Pointee(AggregatableAttributionDataIs( |
| AggregatableHistogramContributionsAre( |
| DefaultAggregatableHistogramContributions( |
| /*histogram_values=*/{5}))))), |
| DroppedEventLevelReportIs( |
| Pointee(EventLevelDataIs(TriggerDataIs(5u)))))); |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| ElementsAre(SourceActiveStateIs( |
| StoredSource::ActiveState::kReachedEventLevelAttributionLimit))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AggregatableTriggerDataOrValuesNotSet_Registered) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetAggregationKeys( |
| *attribution_reporting::AggregationKeys::FromKeys({{"0", 1}})) |
| .Build()); |
| |
| EXPECT_EQ(MaybeCreateAndStoreAggregatableReport( |
| TriggerBuilder() |
| .SetAggregatableTriggerData( |
| {attribution_reporting::AggregatableTriggerData()}) |
| .Build()), |
| AttributionTrigger::AggregatableResult::kNoHistograms); |
| |
| EXPECT_EQ(MaybeCreateAndStoreAggregatableReport( |
| TriggerBuilder() |
| .SetAggregatableValues({*AggregatableValues::Create( |
| {{"0", *AggregatableValuesValue::Create( |
| 123, kDefaultFilteringId)}}, |
| FilterPair())}) |
| .Build()), |
| AttributionTrigger::AggregatableResult::kSuccess); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| PrioritizationConsidersAttributedAndUnattributedSources) { |
| storage()->StoreSource( |
| SourceBuilder().SetSourceEventId(3).SetPriority(10).Build()); |
| |
| ASSERT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetSourceEventId(0).SetPriority(2).Build()); |
| |
| ASSERT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre( |
| EventLevelDataIs( |
| Field(&AttributionReport::EventLevelData::source_event_id, 3)), |
| EventLevelDataIs( |
| Field(&AttributionReport::EventLevelData::source_event_id, 3)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaybeCreateAndStoreEventLevelReport_DeactivatesUnattributedSources) { |
| storage()->StoreSource( |
| SourceBuilder().SetSourceEventId(3).SetPriority(1).Build()); |
| |
| ASSERT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetSourceEventId(7).SetPriority(2).Build()); |
| |
| ASSERT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| ASSERT_THAT(storage()->GetActiveSources(), ElementsAre(SourceEventIdIs(7))); |
| |
| // If the first source were deleted instead of deactivated, this would return |
| // only a single report, as the join against the sources table would fail. |
| ASSERT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre( |
| EventLevelDataIs( |
| Field(&AttributionReport::EventLevelData::source_event_id, 3)), |
| EventLevelDataIs( |
| Field(&AttributionReport::EventLevelData::source_event_id, 7)))); |
| } |
| |
| TEST_F(AttributionResolverTest, AggregationCoordinator_RoundTrip) { |
| auto coordinator_origin = SuitableOrigin::Deserialize("https://a.test"); |
| |
| storage()->StoreSource(TestAggregatableSourceProvider().GetBuilder().Build()); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetAggregationCoordinatorOrigin(*coordinator_origin) |
| .Build(/*generate_event_trigger_data=*/false)), |
| AllOf(CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| NewAggregatableReportIs(Pointee(AggregatableAttributionDataIs( |
| AggregationCoordinatorOriginIs(coordinator_origin)))))); |
| EXPECT_THAT( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max()), |
| ElementsAre(AggregatableAttributionDataIs( |
| AggregationCoordinatorOriginIs(coordinator_origin)))); |
| } |
| |
| TEST_F(AttributionResolverTest, MaxAttributions_BoundedBySourceTimeWindow) { |
| constexpr base::TimeDelta kTimeWindow = base::Days(1); |
| delegate()->set_rate_limits([kTimeWindow]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = kTimeWindow; |
| r.max_source_registration_reporting_origins = |
| std::numeric_limits<int64_t>::max(); |
| r.max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(); |
| r.max_attributions = 1; |
| return r; |
| }()); |
| |
| storage()->StoreSource(SourceBuilder().SetExpiry(base::Days(7)).Build()); |
| |
| AttributionTrigger trigger = DefaultTrigger(); |
| |
| constexpr base::TimeDelta kTriggerDelay = base::Minutes(1); |
| task_environment_.FastForwardBy(kTriggerDelay); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(trigger)); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kExcessiveAttributions, |
| MaybeCreateAndStoreEventLevelReport(trigger)); |
| |
| task_environment_.FastForwardBy(kTimeWindow - kTriggerDelay); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(trigger)); |
| } |
| |
| TEST_F(AttributionResolverTest, NoEventTriggerData_NotRegisteredReturned) { |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build( |
| /*generate_event_trigger_data=*/false)), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNotRegistered), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kNoMatchingImpressions), |
| NewEventLevelReportIs(IsNull()), |
| NewAggregatableReportIs(IsNull()))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, StoreNullAggregatableReport) { |
| base::Time now = base::Time::Now(); |
| base::Time report_time = now + kReportDelay; |
| base::Time fake_source_time = now; |
| |
| delegate()->set_null_aggregatable_reports_lookback_days({0}); |
| AttributionTrigger trigger = DefaultAggregatableTriggerBuilder().Build(); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| delegate()->set_null_aggregatable_reports_lookback_days({}); |
| |
| ASSERT_TRUE(result.min_null_aggregatable_report_time().has_value()); |
| EXPECT_EQ(*result.min_null_aggregatable_report_time(), report_time); |
| |
| AttributionReport expected_report = |
| ReportBuilder(AttributionInfoBuilder( |
| /*context_origin=*/trigger.destination_origin()) |
| .SetTime(now) |
| .Build(), |
| SourceBuilder(fake_source_time).BuildStored()) |
| .SetReportTime(report_time) |
| .BuildNullAggregatable(); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(expected_report)); |
| } |
| |
| TEST_F(AttributionResolverTest, NoAggregatableData_NoNullReport) { |
| delegate()->set_null_aggregatable_reports_lookback_days({0}); |
| auto result = storage()->MaybeCreateAndStoreReport(DefaultTrigger()); |
| delegate()->set_null_aggregatable_reports_lookback_days({}); |
| |
| EXPECT_FALSE(result.min_null_aggregatable_report_time().has_value()); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, BothRealAndNullAggregatableReports) { |
| base::Time now = base::Time::Now(); |
| |
| SourceBuilder builder = TestAggregatableSourceProvider().GetBuilder(now); |
| |
| storage()->StoreSource(builder.Build()); |
| |
| delegate()->set_null_aggregatable_reports_lookback_days({1}); |
| AttributionTrigger trigger = DefaultAggregatableTriggerBuilder().Build( |
| /*generate_event_trigger_data=*/false); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| delegate()->set_null_aggregatable_reports_lookback_days({}); |
| |
| EXPECT_TRUE(result.min_null_aggregatable_report_time().has_value()); |
| EXPECT_EQ(result.aggregatable_status(), |
| AttributionTrigger::AggregatableResult::kSuccess); |
| |
| const AttributionReport expected_null_report = |
| ReportBuilder(AttributionInfoBuilder( |
| /*context_origin=*/trigger.destination_origin()) |
| .SetTime(now) |
| .Build(), |
| SourceBuilder(now - base::Days(1)).BuildStored()) |
| .SetReportTime(now + kReportDelay) |
| .BuildNullAggregatable(); |
| |
| const AttributionReport expected_aggregatable_report = |
| GetExpectedAggregatableReport( |
| builder.SetRemainingAggregatableAttributionBudget(65536 - 1) |
| .BuildStored(), |
| DefaultAggregatableHistogramContributions(), trigger); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre(expected_aggregatable_report, expected_null_report)); |
| } |
| |
| TEST_F(AttributionResolverTest, SourceRegistrationTimeConfig_RoundTrip) { |
| for (auto config : |
| base::EnumSet<attribution_reporting::mojom::SourceRegistrationTimeConfig, |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kMinValue, |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kMaxValue>::All()) { |
| SCOPED_TRACE(config); |
| |
| storage()->StoreSource( |
| TestAggregatableSourceProvider().GetBuilder().Build()); |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetSourceRegistrationTimeConfig(config) |
| .Build(/*generate_event_trigger_data=*/false)), |
| AllOf(CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| NewAggregatableReportIs(Pointee(AggregatableAttributionDataIs( |
| SourceRegistrationTimeConfigIs(config)))))); |
| } |
| } |
| |
| TEST_F(AttributionResolverTest, MaximumAggregatableReportsPerSource) { |
| auto source = TestAggregatableSourceProvider().GetBuilder().Build(); |
| storage()->StoreSource(source); |
| AttributionTrigger trigger = DefaultAggregatableTriggerBuilder().Build(); |
| for (int i = 0; i < 20; i++) { |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kSuccess, |
| MaybeCreateAndStoreAggregatableReport(trigger)); |
| } |
| EXPECT_EQ(AttributionTrigger::AggregatableResult::kExcessiveReports, |
| MaybeCreateAndStoreAggregatableReport(trigger)); |
| } |
| |
| TEST_F(AttributionResolverTest, MaxSourceReportingOriginsPerSite) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.max_reporting_origins_per_source_reporting_site = 1; |
| return r; |
| }()); |
| |
| auto store_source = [&](std::string source, std::string reporting) { |
| return storage() |
| ->StoreSource( |
| SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize(source)) |
| .SetReportingOrigin(*SuitableOrigin::Deserialize(reporting)) |
| .SetExpiry(base::Days(2)) |
| .Build()) |
| .status(); |
| }; |
| store_source("https://a.test", "https://reporter.test"); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| |
| EXPECT_EQ(store_source("https://a.test", "https://a.reporter.test"), |
| StorableSource::Result::kReportingOriginsPerSiteLimitReached); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(1)); |
| |
| store_source("https://b.test", "https://a.reporter.test"); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(2)); |
| |
| store_source("https://b.test", "https://otherreporter.test"); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(3)); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| |
| // After 1 day a new origin can be used. |
| store_source("https://a.test", "https://a.reporter.test"); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(4)); |
| |
| // The reporter used on the first day can't be used even though it is |
| // repeated. |
| EXPECT_EQ(store_source("https://a.test", "https://reporter.test"), |
| StorableSource::Result::kReportingOriginsPerSiteLimitReached); |
| EXPECT_THAT(storage()->GetActiveSources(), SizeIs(4)); |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerDataMatching) { |
| const struct { |
| const char* desc; |
| TriggerDataMatching trigger_data_matching; |
| uint64_t trigger_data; |
| std::optional<uint64_t> expected_trigger_data; |
| } kTestCases[] = { |
| {"modulus-0", TriggerDataMatching::kModulus, 0, 0}, |
| {"modulus-7", TriggerDataMatching::kModulus, 7, 7}, |
| {"modulus-8", TriggerDataMatching::kModulus, 8, 0}, |
| {"modulus-9", TriggerDataMatching::kModulus, 9, 1}, |
| {"exact-0", TriggerDataMatching::kExact, 0, 0}, |
| {"exact-7", TriggerDataMatching::kExact, 7, 7}, |
| {"exact-8", TriggerDataMatching::kExact, 8, std::nullopt}, |
| {"exact-9", TriggerDataMatching::kExact, 9, std::nullopt}, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| SCOPED_TRACE(test_case.desc); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceType( |
| SourceType::kNavigation) // valid trigger data [0, 7] |
| .SetTriggerDataMatching(test_case.trigger_data_matching) |
| .Build()); |
| |
| EXPECT_EQ( |
| test_case.expected_trigger_data.has_value() |
| ? AttributionTrigger::EventLevelResult::kSuccess |
| : AttributionTrigger::EventLevelResult::kNoMatchingTriggerData, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetTriggerData(test_case.trigger_data).Build())); |
| |
| auto reports = storage()->GetAttributionReports(base::Time::Max()); |
| |
| if (std::optional<uint64_t> expected = test_case.expected_trigger_data) { |
| EXPECT_THAT(reports, |
| ElementsAre(EventLevelDataIs(TriggerDataIs(*expected)))); |
| } else { |
| EXPECT_THAT(reports, IsEmpty()); |
| } |
| |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| } |
| } |
| |
| TEST_F(AttributionResolverTest, EventLevelDedupBeforeWindowCheck) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetTriggerSpecs( |
| TriggerSpecs(SourceType::kNavigation, |
| *attribution_reporting::EventReportWindows::Create( |
| base::Milliseconds(0), {base::Hours(1)}), |
| MaxEventLevelReports::Max())) |
| .Build()); |
| |
| ASSERT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetDedupKey(11).Build())); |
| |
| task_environment_.FastForwardBy(base::Hours(1) + base::Microseconds(1)); |
| |
| // Prior to addressing crbug.com/1499913 this returned |
| // `AttributionTrigger::EventLevelResult::kReportWindowPassed` |
| ASSERT_EQ(AttributionTrigger::EventLevelResult::kDeduplicated, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetDedupKey(11).Build())); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AttributionAggregatableReportWithTriggerContextId_RoundTrip) { |
| storage()->StoreSource(TestAggregatableSourceProvider().GetBuilder().Build()); |
| |
| base::Time report_time = base::Time::Now(); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetSourceRegistrationTimeConfig( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kExclude) |
| .SetTriggerContextId("123") |
| .Build(/*generate_event_trigger_data=*/false)), |
| AllOf(CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess), |
| NewAggregatableReportIs(Pointee(AllOf( |
| AggregatableAttributionDataIs( |
| TriggerContextIdIs(Optional(std::string("123")))), |
| ReportTimeIs(report_time)))))); |
| EXPECT_THAT( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max()), |
| ElementsAre(AllOf(AggregatableAttributionDataIs( |
| TriggerContextIdIs(Optional(std::string("123")))), |
| ReportTimeIs(report_time)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| NullAggregatableReportWithTriggerContextId_RoundTrip) { |
| base::Time now = base::Time::Now(); |
| base::Time report_time = now; |
| |
| auto result = storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetSourceRegistrationTimeConfig( |
| attribution_reporting::mojom::SourceRegistrationTimeConfig:: |
| kExclude) |
| .SetTriggerContextId("123") |
| .Build()); |
| |
| ASSERT_TRUE(result.min_null_aggregatable_report_time().has_value()); |
| EXPECT_EQ(*result.min_null_aggregatable_report_time(), report_time); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max()), |
| ElementsAre(AllOf(NullAggregatableDataIs( |
| TriggerContextIdIs(Optional(std::string("123")))), |
| ReportTimeIs(report_time)))); |
| } |
| |
| // TODO(crbug.com/40941848): Support multiple trigger specs instead of just 1. |
| TEST_F(AttributionResolverTest, RejectsMultipleTriggerSpecs) { |
| auto source = SourceBuilder().Build(); |
| source.registration().trigger_specs = *TriggerSpecs::Create( |
| /*trigger_data_indices=*/{{0, 0}}, |
| /*specs=*/{TriggerSpec(), TriggerSpec()}, MaxEventLevelReports::Max()); |
| |
| EXPECT_EQ(storage()->StoreSource(source).status(), |
| StorableSource::Result::kInternalError); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), IsEmpty()); |
| } |
| |
| // Regression test for https://crbug.com/331100922. |
| TEST_F( |
| AttributionResolverTest, |
| FakeSourceCreateAggregatableReport_EffectiveDestinationAttributionRateLimitRecord) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.time_window = base::TimeDelta::Max(); |
| r.max_source_registration_reporting_origins = |
| std::numeric_limits<int64_t>::max(); |
| r.max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(); |
| r.max_attributions = 1; |
| return r; |
| }()); |
| |
| delegate()->set_randomized_response( |
| std::vector<attribution_reporting::FakeEventLevelReport>{}); |
| // This results in event-level attribution rate-limit records for |
| // https://a.test and https://b.test. |
| auto result = storage()->StoreSource( |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://b.test")}) |
| .Build()); |
| EXPECT_EQ(result.status(), StorableSource::Result::kSuccessNoised); |
| delegate()->set_randomized_response(std::nullopt); |
| |
| // This results in one aggregatable attribution rate-limit record for |
| // https://a.test. |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .Build(/*generate_event_trigger_data=*/false)), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNotRegistered), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .Build(/*generate_event_trigger_data=*/false)), |
| AllOf( |
| CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNotRegistered), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kExcessiveAttributions))); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://b.test")) |
| .Build(/*generate_event_trigger_data=*/false)), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kNotRegistered), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| AttributedTriggerIncludeSourceRegistrationTime_NullAggregatableReports) { |
| SourceBuilder builder = TestAggregatableSourceProvider().GetBuilder(); |
| storage()->StoreSource(builder.Build()); |
| |
| const base::Time now = base::Time::Now(); |
| |
| const auto trigger = DefaultAggregatableTriggerBuilder() |
| .SetSourceRegistrationTimeConfig( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kInclude) |
| .Build(/*generate_event_trigger_data=*/false); |
| delegate()->set_null_aggregatable_reports_lookback_days({0, 1, 30, 31}); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| delegate()->set_null_aggregatable_reports_lookback_days({}); |
| |
| EXPECT_THAT(result.min_null_aggregatable_report_time(), |
| Optional(now + kReportDelay)); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre( |
| AggregatableAttributionDataIs(SourceRegistrationTimeConfigIs( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kInclude)), |
| NullAggregatableDataIs( |
| AllOf(SourceRegistrationTimeConfigIs( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kInclude), |
| SourceTimeIs(now - base::Days(1)))), |
| NullAggregatableDataIs( |
| AllOf(SourceRegistrationTimeConfigIs( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kInclude), |
| SourceTimeIs(now - base::Days(30)))))); |
| } |
| |
| TEST_F( |
| AttributionResolverTest, |
| UnattributedTriggerIncludeSourceRegistrationTime_NullAggregatableReports) { |
| const base::Time now = base::Time::Now(); |
| |
| const auto trigger = DefaultAggregatableTriggerBuilder() |
| .SetSourceRegistrationTimeConfig( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kInclude) |
| .Build(/*generate_event_trigger_data=*/false); |
| delegate()->set_null_aggregatable_reports_lookback_days({0, 1, 30, 31}); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| delegate()->set_null_aggregatable_reports_lookback_days({}); |
| |
| EXPECT_THAT(result.min_null_aggregatable_report_time(), |
| Optional(now + kReportDelay)); |
| |
| EXPECT_THAT( |
| storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre(NullAggregatableDataIs(AllOf( |
| SourceRegistrationTimeConfigIs( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kInclude), |
| SourceTimeIs(now))), |
| NullAggregatableDataIs(AllOf( |
| SourceRegistrationTimeConfigIs( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kInclude), |
| SourceTimeIs(now - base::Days(1)))), |
| NullAggregatableDataIs(AllOf( |
| SourceRegistrationTimeConfigIs( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kInclude), |
| SourceTimeIs(now - base::Days(30)))))); |
| } |
| |
| TEST_F( |
| AttributionResolverTest, |
| AttributedTriggerExcludeSourceRegistrationTime_NoNullAggregatableReport) { |
| SourceBuilder builder = TestAggregatableSourceProvider().GetBuilder(); |
| storage()->StoreSource(builder.Build()); |
| |
| const auto trigger = DefaultAggregatableTriggerBuilder() |
| .SetSourceRegistrationTimeConfig( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kExclude) |
| .Build(/*generate_event_trigger_data=*/false); |
| delegate()->set_null_aggregatable_reports_lookback_days({0, 1, 30, 31}); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| delegate()->set_null_aggregatable_reports_lookback_days({}); |
| |
| EXPECT_THAT(result.min_null_aggregatable_report_time(), Eq(std::nullopt)); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre( |
| AggregatableAttributionDataIs(SourceRegistrationTimeConfigIs( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kExclude)))); |
| } |
| |
| TEST_F( |
| AttributionResolverTest, |
| UnattributedTriggerExcludeSourceRegistrationTime_NullAggregatableReport) { |
| const base::Time now = base::Time::Now(); |
| |
| const auto trigger = DefaultAggregatableTriggerBuilder() |
| .SetSourceRegistrationTimeConfig( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kExclude) |
| .Build(/*generate_event_trigger_data=*/false); |
| delegate()->set_null_aggregatable_reports_lookback_days({0, 1, 30, 31}); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| delegate()->set_null_aggregatable_reports_lookback_days({}); |
| |
| EXPECT_THAT(result.min_null_aggregatable_report_time(), |
| Optional(now + kReportDelay)); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre(NullAggregatableDataIs( |
| AllOf(SourceRegistrationTimeConfigIs( |
| attribution_reporting::mojom:: |
| SourceRegistrationTimeConfig::kExclude), |
| SourceTimeIs(now))))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| SourceAggregatableDebugReportingConfig_RoundTrips) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetAggregatableDebugReportingConfig( |
| *attribution_reporting::SourceAggregatableDebugReportingConfig:: |
| Create( |
| /*budget=*/10, |
| attribution_reporting::AggregatableDebugReportingConfig( |
| /*key_piece=*/123, /*debug_data=*/{}, |
| /*aggregation_coordinator_origin=*/std::nullopt))) |
| .Build()); |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| ElementsAre(AllOf( |
| Property(&StoredSource::remaining_aggregatable_debug_budget, 10), |
| Property(&StoredSource::aggregatable_debug_key_piece, 123), |
| RemainingAggregatableAttributionBudgetIs(65536 - 10)))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ProcessAggregatableDebugReport_NoBudgetAndNoSourceId) { |
| // Insufficient budget, null report. |
| EXPECT_THAT( |
| storage()->ProcessAggregatableDebugReport( |
| CreateAggregatableDebugReport( |
| {AggregatableReportHistogramContribution( |
| /*bucket=*/1, /*value=*/65536, |
| /*filtering_id=*/std::nullopt), |
| AggregatableReportHistogramContribution( |
| /*bucket=*/2, /*value=*/1, /*filtering_id=*/std::nullopt)}), |
| /*remaining_budget=*/std::nullopt, |
| /*source_id=*/std::nullopt), |
| AllOf(Field(&ProcessAggregatableDebugReportResult::report, |
| Property(&AggregatableDebugReport::contributions, IsEmpty())), |
| Field(&ProcessAggregatableDebugReportResult::result, |
| ProcessAggregatableDebugReportStatus::kInsufficientBudget))); |
| |
| // Adjusts rate limits. |
| EXPECT_THAT( |
| storage()->ProcessAggregatableDebugReport( |
| CreateAggregatableDebugReport( |
| {AggregatableReportHistogramContribution( |
| /*bucket=*/1, /*value=*/65535, |
| /*filtering_id=*/std::nullopt), |
| AggregatableReportHistogramContribution( |
| /*bucket=*/2, /*value=*/1, /*filtering_id=*/std::nullopt)}), |
| /*remaining_budget=*/std::nullopt, |
| /*source_id=*/std::nullopt), |
| AllOf(Field(&ProcessAggregatableDebugReportResult::report, |
| Property(&AggregatableDebugReport::contributions, SizeIs(2))), |
| Field(&ProcessAggregatableDebugReportResult::result, |
| ProcessAggregatableDebugReportStatus::kSuccess))); |
| |
| // Hits rate limits, null report. |
| EXPECT_THAT( |
| storage()->ProcessAggregatableDebugReport( |
| CreateAggregatableDebugReport( |
| {AggregatableReportHistogramContribution( |
| /*bucket=*/1, /*value=*/1, /*filtering_id=*/std::nullopt)}), |
| /*remaining_budget=*/std::nullopt, |
| /*source_id=*/std::nullopt), |
| AllOf(Field(&ProcessAggregatableDebugReportResult::report, |
| Property(&AggregatableDebugReport::contributions, IsEmpty())), |
| Field(&ProcessAggregatableDebugReportResult::result, |
| ProcessAggregatableDebugReportStatus:: |
| kReportingSiteRateLimitReached))); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| ProcessAggregatableDebugReport_BudgetAndNoSourceId) { |
| // Insufficient budget, null report. |
| EXPECT_THAT( |
| storage()->ProcessAggregatableDebugReport( |
| CreateAggregatableDebugReport( |
| {AggregatableReportHistogramContribution( |
| /*bucket=*/1, /*value=*/1000, /*filtering_id=*/std::nullopt), |
| AggregatableReportHistogramContribution( |
| /*bucket=*/2, /*value=*/1, /*filtering_id=*/std::nullopt)}), |
| /*remaining_budget=*/1000, |
| /*source_id=*/std::nullopt), |
| AllOf(Field(&ProcessAggregatableDebugReportResult::report, |
| Property(&AggregatableDebugReport::contributions, IsEmpty())), |
| Field(&ProcessAggregatableDebugReportResult::result, |
| ProcessAggregatableDebugReportStatus::kInsufficientBudget))); |
| |
| // Adjusts rate limits. |
| EXPECT_THAT( |
| storage()->ProcessAggregatableDebugReport( |
| CreateAggregatableDebugReport( |
| {AggregatableReportHistogramContribution( |
| /*bucket=*/1, /*value=*/999, /*filtering_id=*/std::nullopt), |
| AggregatableReportHistogramContribution( |
| /*bucket=*/2, /*value=*/1, /*filtering_id=*/std::nullopt)}), |
| /*remaining_budget=*/1000, |
| /*source_id=*/std::nullopt), |
| AllOf(Field(&ProcessAggregatableDebugReportResult::report, |
| Property(&AggregatableDebugReport::contributions, SizeIs(2))), |
| Field(&ProcessAggregatableDebugReportResult::result, |
| ProcessAggregatableDebugReportStatus::kSuccess))); |
| |
| // Hits rate limits, null report. |
| EXPECT_THAT( |
| storage()->ProcessAggregatableDebugReport( |
| CreateAggregatableDebugReport( |
| {AggregatableReportHistogramContribution( |
| /*bucket=*/1, /*value=*/64537, |
| /*filtering_id=*/std::nullopt)}), |
| /*remaining_budget=*/65536, |
| /*source_id=*/std::nullopt), |
| AllOf(Field(&ProcessAggregatableDebugReportResult::report, |
| Property(&AggregatableDebugReport::contributions, IsEmpty())), |
| Field(&ProcessAggregatableDebugReportResult::result, |
| ProcessAggregatableDebugReportStatus:: |
| kReportingSiteRateLimitReached))); |
| } |
| |
| TEST_F(AttributionResolverTest, ProcessAggregatableDebugReport_SourceId) { |
| delegate()->set_aggregatable_debug_rate_limit({ |
| .max_budget_per_context_site = 65537, |
| .max_budget_per_context_reporting_site = 65536, |
| .max_reports_per_source = 2, |
| }); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetAggregatableDebugReportingConfig( |
| *attribution_reporting::SourceAggregatableDebugReportingConfig:: |
| Create( |
| /*budget=*/1000, |
| attribution_reporting::AggregatableDebugReportingConfig( |
| /*key_piece=*/1, /*debug_data=*/{}, |
| /*aggregation_coordinator_origin=*/std::nullopt))) |
| .Build()); |
| |
| const struct { |
| std::optional<int> remaining_budget; |
| std::optional<StoredSource::Id> source_id; |
| int consumed_budget; |
| const char* reporting_origin = "https://r.test"; |
| bool expected_valid; |
| ProcessAggregatableDebugReportStatus expected_result; |
| } kInputs[] = { |
| // Remaining budget not matching stored data. |
| { |
| .remaining_budget = 990, |
| .source_id = StoredSource::Id(1), |
| .consumed_budget = 990, |
| .expected_valid = false, |
| .expected_result = |
| ProcessAggregatableDebugReportStatus::kInternalError, |
| }, |
| { |
| .remaining_budget = 1000, |
| .source_id = StoredSource::Id(1), |
| .consumed_budget = 990, |
| .expected_valid = true, |
| .expected_result = ProcessAggregatableDebugReportStatus::kSuccess, |
| }, |
| // Not counted for the limits. |
| { |
| .source_id = StoredSource::Id(1), |
| .consumed_budget = 0, |
| .expected_valid = false, |
| .expected_result = ProcessAggregatableDebugReportStatus::kNoDebugData, |
| }, |
| { |
| .source_id = StoredSource::Id(1), |
| .consumed_budget = 11, |
| .expected_valid = false, |
| .expected_result = |
| ProcessAggregatableDebugReportStatus::kInsufficientBudget, |
| }, |
| { |
| .source_id = StoredSource::Id(1), |
| .consumed_budget = 9, |
| .expected_valid = true, |
| .expected_result = ProcessAggregatableDebugReportStatus::kSuccess, |
| }, |
| { |
| .source_id = StoredSource::Id(1), |
| .consumed_budget = 1, |
| .expected_valid = false, |
| .expected_result = |
| ProcessAggregatableDebugReportStatus::kExcessiveReports, |
| }, |
| { |
| .consumed_budget = 64539, |
| .expected_valid = false, |
| .expected_result = |
| ProcessAggregatableDebugReportStatus::kBothRateLimitsReached, |
| }, |
| { |
| .consumed_budget = 64538, |
| .expected_valid = false, |
| .expected_result = ProcessAggregatableDebugReportStatus:: |
| kReportingSiteRateLimitReached, |
| }, |
| { |
| .consumed_budget = 64539, |
| .reporting_origin = "https://r1.test", |
| .expected_valid = false, |
| .expected_result = |
| ProcessAggregatableDebugReportStatus::kGlobalRateLimitReached, |
| }, |
| }; |
| |
| for (const auto& input : kInputs) { |
| base::HistogramTester histograms; |
| |
| std::vector<AggregatableReportHistogramContribution> contributions; |
| if (input.consumed_budget > 0) { |
| contributions.emplace_back(/*bucket=*/1, /*value=*/input.consumed_budget, |
| /*filtering_id=*/std::nullopt); |
| } |
| |
| EXPECT_THAT(storage()->ProcessAggregatableDebugReport( |
| CreateAggregatableDebugReport(std::move(contributions), |
| input.reporting_origin), |
| input.remaining_budget, input.source_id), |
| AllOf(Field(&ProcessAggregatableDebugReportResult::report, |
| Property(&AggregatableDebugReport::contributions, |
| SizeIs(input.expected_valid))), |
| Field(&ProcessAggregatableDebugReportResult::result, |
| input.expected_result))); |
| histograms.ExpectUniqueSample( |
| "Conversions.AggregatableDebugReport.ProcessResult", |
| input.expected_result, 1); |
| } |
| } |
| |
| TEST_F(AttributionResolverTest, PerDayLimitReached_SourceDropped) { |
| delegate()->set_destination_rate_limit({ |
| .max_per_reporting_site_per_day = 1, |
| }); |
| |
| EXPECT_EQ(storage() |
| ->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d1.test")}) |
| .Build()) |
| .status(), |
| StorableSource::Result::kSuccess); |
| EXPECT_EQ(storage() |
| ->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d2.test")}) |
| .Build()) |
| .status(), |
| StorableSource::Result::kDestinationPerDayReportingLimitReached); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| |
| EXPECT_EQ(storage() |
| ->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites({net::SchemefulSite::Deserialize( |
| "https://d2.test")}) |
| .Build()) |
| .status(), |
| StorableSource::Result::kSuccess); |
| } |
| |
| TEST_F(AttributionResolverTest, LimitHit_DestinationDeactivated) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(1) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .Build()), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Eq(std::nullopt)))); |
| |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d2.test")}) |
| .Build()), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Optional(1)))); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(SourceEventIdIs(2))); |
| } |
| |
| TEST_F(AttributionResolverTest, PriorityTooLow_SourceDropped) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(1) |
| .SetDestinationLimitPriority(1) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .Build()), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Eq(std::nullopt)))); |
| |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d2.test")}) |
| .Build()), |
| Property( |
| &StoreSourceResult::result, |
| VariantWith< |
| StoreSourceResult::InsufficientUniqueDestinationCapacity>(Field( |
| &StoreSourceResult::InsufficientUniqueDestinationCapacity::limit, |
| 1)))); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(SourceEventIdIs(1))); |
| } |
| |
| TEST_F(AttributionResolverTest, LimitHit_EventLevelReportNotDeleted) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(1) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .Build()), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Eq(std::nullopt)))); |
| |
| EXPECT_EQ(MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://d1.test")) |
| .Build()), |
| AttributionTrigger::EventLevelResult::kSuccess); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| // This should deactivate the source, but doesn't delete the pending report. |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d2.test")}) |
| .Build()), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Optional(1)))); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(SourceEventIdIs(2))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), SizeIs(1)); |
| } |
| |
| TEST_F(AttributionResolverTest, LimitHit_AggregatableReportDeleted) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.max_attributions = 1; |
| return r; |
| }()); |
| |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| |
| StorableSource source = |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .Build(); |
| AttributionTrigger trigger = |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin(*SuitableOrigin::Deserialize("https://d1.test")) |
| .Build( |
| /*generate_event_trigger_data=*/false); |
| |
| EXPECT_THAT( |
| storage()->StoreSource(source), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Eq(std::nullopt)))); |
| |
| EXPECT_EQ(MaybeCreateAndStoreAggregatableReport(trigger), |
| AttributionTrigger::AggregatableResult::kSuccess); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), SizeIs(1)); |
| EXPECT_EQ(MaybeCreateAndStoreAggregatableReport(trigger), |
| AttributionTrigger::AggregatableResult::kExcessiveAttributions); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| // This should deactivate the previous source, delete the pending report, and |
| // the corresponding attribution rate-limit record. |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d2.test")}) |
| .Build()), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Optional(1)))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| EXPECT_THAT( |
| storage()->StoreSource(source), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Optional(1)))); |
| EXPECT_EQ(MaybeCreateAndStoreAggregatableReport(trigger), |
| AttributionTrigger::AggregatableResult::kSuccess); |
| } |
| |
| TEST_F(AttributionResolverTest, LimitHit_FakeReportDeleted) { |
| delegate()->set_rate_limits([]() { |
| AttributionConfig::RateLimitConfig r; |
| r.max_attributions = 1; |
| return r; |
| }()); |
| |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| |
| delegate()->set_randomized_response( |
| std::vector<attribution_reporting::FakeEventLevelReport>{ |
| {.trigger_data = 0, .window_index = 0}, |
| {.trigger_data = 1, .window_index = 1}}); |
| |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .SetTriggerSpecs(TriggerSpecs( |
| SourceType::kEvent, |
| *attribution_reporting::EventReportWindows::Create( |
| base::Days(0), {base::Days(1), base::Days(2)}), |
| MaxEventLevelReports::Max())) |
| .Build()), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Eq(std::nullopt)))); |
| delegate()->set_randomized_response(std::nullopt); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre(EventLevelDataIs(TriggerDataIs(0)), |
| EventLevelDataIs(TriggerDataIs(1)))); |
| |
| StorableSource source = |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .Build(); |
| AttributionTrigger trigger = |
| TriggerBuilder() |
| .SetDestinationOrigin(*SuitableOrigin::Deserialize("https://d1.test")) |
| .Build(); |
| |
| EXPECT_THAT( |
| storage()->StoreSource(source), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Eq(std::nullopt)))); |
| EXPECT_EQ(MaybeCreateAndStoreEventLevelReport(trigger), |
| AttributionTrigger::EventLevelResult::kExcessiveAttributions); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| // This should deactivate the sources and delete the second fake report, but |
| // not deleting the corresponding attribution rate-limit record. |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d2.test")}) |
| .Build()), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Optional(1)))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre(EventLevelDataIs(TriggerDataIs(0)))); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| EXPECT_THAT( |
| storage()->StoreSource(source), |
| AllOf(Property(&StoreSourceResult::result, |
| VariantWith<StoreSourceResult::Success>(_)), |
| Property(&StoreSourceResult::destination_limit, Optional(1)))); |
| EXPECT_EQ(MaybeCreateAndStoreEventLevelReport(trigger), |
| AttributionTrigger::EventLevelResult::kExcessiveAttributions); |
| } |
| |
| TEST_F( |
| AttributionResolverTest, |
| LimitHitAndDestinationGlobalRateLimitHit_DestinationDeactivatedAndSourceDropped) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| delegate()->set_destination_rate_limit([]() { |
| AttributionConfig::DestinationRateLimit limit; |
| limit.max_total = 1; |
| limit.rate_limit_window = base::Minutes(1); |
| return limit; |
| }()); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .Build()); |
| |
| EXPECT_THAT( |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d2.test")}) |
| .Build()), |
| AllOf( |
| Property( |
| &StoreSourceResult::result, |
| VariantWith<StoreSourceResult::DestinationGlobalLimitReached>(_)), |
| Property(&StoreSourceResult::destination_limit, Optional(1)))); |
| EXPECT_THAT(storage()->GetActiveSources(), IsEmpty()); |
| } |
| |
| TEST_F(AttributionResolverTest, DestinationLimitResultMetrics) { |
| delegate()->set_max_destinations_per_source_site_reporting_site(1); |
| |
| StorableSource source = |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d1.test")}) |
| .Build(); |
| |
| const struct { |
| const char* desc; |
| const char* destination; |
| int64_t priority = 0; |
| int expected; |
| } kTestCases[] = { |
| { |
| .desc = "allowed", |
| .destination = "https://d1.test", |
| .expected = 0, // kAllowed |
| }, |
| { |
| .desc = "allowed-limit-hit", |
| .destination = "https://d2.test", |
| .priority = 1, |
| .expected = 1, // kAllowedLimitHit |
| }, |
| { |
| .desc = "not-allowed", |
| .destination = "https://d2.test", |
| .priority = -1, |
| .expected = 2, // kNotAllowed |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| SCOPED_TRACE(test_case.desc); |
| |
| storage()->StoreSource(source); |
| |
| base::HistogramTester histograms; |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationLimitPriority(test_case.priority) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize(test_case.destination)}) |
| .Build()); |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| histograms.ExpectBucketCount("Conversions.SourceDestinationLimitResult", |
| test_case.expected, 1); |
| } |
| } |
| |
| TEST_F(AttributionResolverTest, SourceAttributionScopesData_RoundTrips) { |
| auto scopes = *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1", "2"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u); |
| storage()->StoreSource( |
| SourceBuilder().SetAttributionScopesData(scopes).Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(AttributionScopesDataIs(scopes))); |
| } |
| |
| TEST_F(AttributionResolverTest, SourcesWithDifferentAttributionScopeLimits) { |
| // Default source, should be deleted once a source with scopes is registered. |
| EXPECT_EQ(storage() |
| ->StoreSource(SourceBuilder().SetSourceEventId(1).Build()) |
| .status(), |
| StorableSource::Result::kSuccess); |
| // Should not be deleted along with its respective source as only reports with |
| // trigger time >= current time are deleted. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| // Should delete the first source as that has no scopes. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), ElementsAre(SourceEventIdIs(2u))); |
| |
| // Should be stored initially. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(3) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://conversion.test")}) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/2u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| // Should be deleted once the respective source is deleted. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .SetAttributionScopes( |
| attribution_reporting::AttributionScopesSet({"1", "2"})) |
| .SetTriggerData(1) |
| .Build())); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), SizeIs(2)); |
| |
| // Should remain in storage. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(4) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://conversion2.test")}) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/2u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(SourceEventIdIs(2u), SourceEventIdIs(3u), |
| SourceEventIdIs(4u))); |
| |
| // Should delete the third source as that has a lower scope limit. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(5) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(SourceEventIdIs(2u), SourceEventIdIs(4u), |
| SourceEventIdIs(5u))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| UnorderedElementsAre(EventLevelDataIs(TriggerDataIs(7)))); |
| } |
| |
| TEST_F(AttributionResolverTest, IncomingEmptyScopes_RemovesOtherScopes) { |
| auto scopes = *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/4u); |
| storage()->StoreSource(SourceBuilder() |
| .SetSourceEventId(1) |
| .SetAttributionScopesData(scopes) |
| .Build()); |
| |
| // Should not be modified as it does not share the same destination site. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test")}) |
| .SetAttributionScopesData(scopes) |
| .Build()); |
| |
| // Should not be modified as it does not share the same reporting origin. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(3) |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://a.test")) |
| .SetAttributionScopesData(scopes) |
| .Build()); |
| |
| // Should remove only the first source's scopes data. |
| storage()->StoreSource(SourceBuilder().SetSourceEventId(4).Build()); |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| UnorderedElementsAre( |
| AllOf(SourceEventIdIs(1u), AttributionScopesDataIs(std::nullopt)), |
| AllOf(SourceEventIdIs(2u), AttributionScopesDataIs(scopes)), |
| AllOf(SourceEventIdIs(3u), AttributionScopesDataIs(scopes)), |
| AllOf(SourceEventIdIs(4u), AttributionScopesDataIs(std::nullopt)))); |
| } |
| |
| TEST_F(AttributionResolverTest, SourcesWithDifferentMaxEventStates) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(1) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://b.test")}) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize("https://a.test")) |
| .SetAttributionScopes( |
| attribution_reporting::AttributionScopesSet({"1", "2"})) |
| .Build())); |
| |
| // Should remain in storage. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://a.test"), |
| net::SchemefulSite::Deserialize("https://c.test")}) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| // Should delete the first source. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(3) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.test")}) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/2u)) |
| .Build()); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceEventIdIs(2u), SourceEventIdIs(3u))); |
| |
| // Should delete the third source. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(4) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.test")}) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/4u)) |
| .Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceEventIdIs(2u), SourceEventIdIs(4u))); |
| } |
| |
| TEST_F(AttributionResolverTest, RemoveOutdatedScopes) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(1) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1", "2"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| // Should be deleted along with source 1. |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetAttributionScopes( |
| attribution_reporting::AttributionScopesSet({"1", "4"})) |
| .Build())); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"3", "4"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(3) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"3", "5"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| ElementsAre( |
| AllOf(SourceEventIdIs(2u), |
| AttributionScopesDataIs(AttributionScopesSetIs( |
| attribution_reporting::AttributionScopesSet({"3", "4"})))), |
| AllOf( |
| SourceEventIdIs(3u), |
| AttributionScopesDataIs(AttributionScopesSetIs( |
| attribution_reporting::AttributionScopesSet({"3", "5"})))))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| |
| // This will delete sources 2 and 3 as the list of allowed scopes becomes |
| // `{"2", "1", "5", "4"}` due to prioritizing latest source time first. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(4) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1", "2"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| EXPECT_THAT( |
| storage()->GetActiveSources(), |
| ElementsAre(AllOf( |
| SourceEventIdIs(4u), |
| AttributionScopesDataIs(AttributionScopesSetIs( |
| attribution_reporting::AttributionScopesSet({"1", "2"})))))); |
| } |
| |
| TEST_F(AttributionResolverTest, RemoveOutdatedScopes_RetainTop) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(1) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"2", "3"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(3) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"4", "5"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| // 5 scopes are already stored; This source's 3 scopes should be retained. |
| // Therefore, we expect `SelectScopes()` to find the top 2 scopes to retain |
| // instead of the bottom 3 scopes to remove. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(4) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"6", "7", "8"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(SourceEventIdIs(3u), SourceEventIdIs(4u))); |
| } |
| |
| TEST_F(AttributionResolverTest, RemoveOutdatedScopes_RemoveBottom) { |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(1) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"2", "3"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(3) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"4", "5"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| // 5 scopes are already stored; This source's 2 scopes should be retained. |
| // Therefore, we expect `SelectScopes()` to find the bottom 2 scopes to remove |
| // instead of the top 3 scopes to retain. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(4) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"6", "7"}), |
| /*attribution_scope_limit=*/5u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(SourceEventIdIs(3u), SourceEventIdIs(4u))); |
| } |
| |
| TEST_F(AttributionResolverTest, TriggerAttributesOnMatchingScope) { |
| // Should be attributed. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(1) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"1", "2"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| // Should be deleted. |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceEventId(2) |
| .SetPriority(5) |
| .SetAttributionScopesData( |
| *attribution_reporting::AttributionScopesData::Create( |
| attribution_reporting::AttributionScopesSet({"3", "4"}), |
| /*attribution_scope_limit=*/4u, |
| /*max_event_states=*/3u)) |
| .Build()); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kNoMatchingImpressions, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetAttributionScopes( |
| attribution_reporting::AttributionScopesSet({"5"})) |
| .Build())); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetAttributionScopes( |
| attribution_reporting::AttributionScopesSet({"6", "2"})) |
| .Build())); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kNoMatchingImpressions, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder() |
| .SetAttributionScopes( |
| attribution_reporting::AttributionScopesSet({"3"})) |
| .Build())); |
| EXPECT_THAT( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max()), |
| ElementsAre(EventLevelDataIs( |
| Field(&AttributionReport::EventLevelData::source_event_id, 1u)))); |
| } |
| |
| TEST_F(AttributionResolverTest, DebugKey) { |
| const struct { |
| std::optional<uint64_t> source_debug_key; |
| std::optional<uint64_t> trigger_debug_key; |
| int expected_metric; |
| } kTestCases[] = { |
| { |
| std::nullopt, std::nullopt, |
| 0, // kNone |
| }, |
| { |
| 1, std::nullopt, |
| 1, // kSourceOnly |
| }, |
| { |
| std::nullopt, 1, |
| 2, // kTriggerOnly |
| }, |
| { |
| 1, 2, |
| 3, // kBoth |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| base::HistogramTester histograms; |
| storage()->StoreSource(TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetDebugKey(test_case.source_debug_key) |
| .SetCookieBasedDebugAllowed(true) |
| .Build()); |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDebugKey(test_case.trigger_debug_key) |
| .Build()); |
| histograms.ExpectBucketCount("Conversions.AttributionReportDebugKeyUsage", |
| test_case.expected_metric, 2); |
| } |
| } |
| |
| TEST_F(AttributionResolverTest, |
| UniqueReportingOriginsPerSiteForAttributionMetric) { |
| base::HistogramTester histogram_tester; |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d.test")}) |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://a.r.test")) |
| .Build()); |
| storage()->StoreSource( |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://d.test")}) |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://b.r.test")) |
| .Build()); |
| |
| storage()->MaybeCreateAndStoreReport( |
| TriggerBuilder() |
| .SetDestinationOrigin(*SuitableOrigin::Deserialize("https://d.test")) |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://a.r.test")) |
| .Build()); |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin(*SuitableOrigin::Deserialize("https://d.test")) |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://b.r.test")) |
| .Build(/*generate_event_trigger_data=*/false)); |
| // No histogram recorded as no attribution report was created. |
| storage()->MaybeCreateAndStoreReport(TriggerBuilder().Build()); |
| |
| EXPECT_THAT(histogram_tester.GetAllSamples( |
| "Conversions.UniqueReportingOriginsPerSiteForAttribution"), |
| base::BucketsAre(base::Bucket(1, 1), base::Bucket(2, 1))); |
| } |
| |
| TEST_F(AttributionResolverTest, SourceAggregatableNamedBudgets_RoundTrips) { |
| auto budgets = |
| *attribution_reporting::AggregatableNamedBudgetDefs::FromBudgetMap({ |
| {"a", 5}, |
| }); |
| storage()->StoreSource( |
| SourceBuilder().SetAggregatableNamedBudgetDefs(budgets).Build()); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| UnorderedElementsAre(AggregatableNamedBudgetsIs( |
| StoredSource::AggregatableNamedBudgets( |
| {{"a", *AggregatableNamedBudgetPair::Create(5, 5)}})))); |
| } |
| |
| TEST_F(AttributionResolverTest, MaxAggregatableBudgetPerNamedBudgetPerSource) { |
| storage()->StoreSource( |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetAggregatableNamedBudgetDefs( |
| *attribution_reporting::AggregatableNamedBudgetDefs:: |
| FromBudgetMap({{"a", 5}, {"b", 5}, {"c", 0}})) |
| .Build()); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{7}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "a", attribution_reporting::FilterPair())}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kInsufficientNamedBudget)); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{5}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "a", attribution_reporting::FilterPair())}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess)); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{1}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "a", attribution_reporting::FilterPair())}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kInsufficientNamedBudget)); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{5}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "b", attribution_reporting::FilterPair())}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess)); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{5}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "c", attribution_reporting::FilterPair())}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kInsufficientNamedBudget)); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{5}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "d", attribution_reporting::FilterPair())}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess)); |
| } |
| |
| TEST_F(AttributionResolverTest, |
| MaxAggregatableBudgetPerNamedBudgetPerFilteredSource) { |
| storage()->StoreSource( |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetFilterData(*FilterData::Create({{"abc", {"123"}}})) |
| .SetAggregatableNamedBudgetDefs( |
| *attribution_reporting::AggregatableNamedBudgetDefs:: |
| FromBudgetMap({{"a", 10}, {"b", 5}})) |
| .Build()); |
| |
| // Different filters should not match named buckets together. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{11}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "a", FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"456"}}, |
| })}, |
| /*negative=*/{}))}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess)); |
| |
| // No named bucket is matched as the provided name is null. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{11}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| /*name=*/std::nullopt, |
| FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"123"}}, |
| })}, |
| /*negative=*/{}))}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess)); |
| |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{11}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "a", FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"123"}}, |
| })}, |
| /*negative=*/{}))}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kInsufficientNamedBudget)); |
| |
| // First named budget ignored, second used. |
| EXPECT_THAT( |
| storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder( |
| /*histogram_values=*/{7}) |
| .SetAggregatableNamedBudgetCandidates( |
| {attribution_reporting::AggregatableNamedBudgetCandidate( |
| "a", FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"456"}}, |
| })}, |
| /*negative=*/{})), |
| attribution_reporting::AggregatableNamedBudgetCandidate( |
| "b", FilterPair(/*positive=*/{*FilterConfig::Create({ |
| {"abc", {"123"}}, |
| })}, |
| /*negative=*/{}))}) |
| .Build()), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kInsufficientNamedBudget)); |
| } |
| |
| } // namespace content |