| // 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_storage_sql.h" |
| |
| #include <stdint.h> |
| |
| #include <functional> |
| #include <limits> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/functional/overloaded.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/run_loop.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "components/aggregation_service/aggregation_service.mojom.h" |
| #include "components/attribution_reporting/destination_set.h" |
| #include "components/attribution_reporting/filters.h" |
| #include "components/attribution_reporting/source_registration.h" |
| #include "components/attribution_reporting/source_type.mojom.h" |
| #include "components/attribution_reporting/suitable_origin.h" |
| #include "content/browser/attribution_reporting/aggregatable_histogram_contribution.h" |
| #include "content/browser/attribution_reporting/attribution_constants.h" |
| #include "content/browser/attribution_reporting/attribution_report.h" |
| #include "content/browser/attribution_reporting/attribution_reporting.pb.h" |
| #include "content/browser/attribution_reporting/attribution_test_utils.h" |
| #include "content/browser/attribution_reporting/attribution_trigger.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 "net/base/schemeful_site.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/trigger_verification.h" |
| #include "sql/database.h" |
| #include "sql/meta_table.h" |
| #include "sql/statement.h" |
| #include "sql/test/scoped_error_expecter.h" |
| #include "sql/test/test_helpers.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| using ::attribution_reporting::SuitableOrigin; |
| |
| using ::testing::AllOf; |
| using ::testing::ElementsAre; |
| using ::testing::IsEmpty; |
| using ::testing::Pair; |
| using ::testing::SizeIs; |
| |
| struct AttributionReportRecord { |
| int64_t report_id; |
| int64_t source_id; |
| base::Time trigger_time; |
| base::Time report_time; |
| base::Time initial_report_time; |
| int failed_send_attempts = 0; |
| std::string external_report_id; |
| absl::optional<uint64_t> debug_key; |
| std::string context_origin = "https://destination.test"; |
| std::string reporting_origin = "https://reporter.test"; |
| int report_type; |
| std::string metadata; |
| }; |
| |
| struct AttributionEventLevelMetadataRecord { |
| absl::optional<uint64_t> trigger_data; |
| absl::optional<int64_t> priority; |
| }; |
| |
| struct AttributionAggregatableMetadataRecord { |
| struct Contribution { |
| absl::optional<uint64_t> high_bits; |
| absl::optional<uint64_t> low_bits; |
| absl::optional<uint32_t> value; |
| }; |
| absl::optional<::aggregation_service::mojom::AggregationCoordinator> |
| coordinator = |
| ::aggregation_service::mojom::AggregationCoordinator::kAwsCloud; |
| std::vector<Contribution> contributions; |
| absl::optional< |
| proto::AttributionCommonAggregatableMetadata_SourceRegistrationTimeConfig> |
| source_registration_time_config = |
| proto::AttributionCommonAggregatableMetadata::INCLUDE; |
| }; |
| |
| struct AttributionNullAggregatableMetadataRecord { |
| absl::optional<::aggregation_service::mojom::AggregationCoordinator> |
| coordinator = |
| ::aggregation_service::mojom::AggregationCoordinator::kAwsCloud; |
| absl::optional<int64_t> fake_source_time; |
| absl::optional< |
| proto::AttributionCommonAggregatableMetadata_SourceRegistrationTimeConfig> |
| source_registration_time_config = |
| proto::AttributionCommonAggregatableMetadata::INCLUDE; |
| }; |
| |
| std::string CreateSerializedFilterData( |
| const attribution_reporting::FilterValues& filter_values) { |
| proto::AttributionFilterData msg; |
| |
| for (const auto& [filter, values] : filter_values) { |
| proto::AttributionFilterValues filter_values_msg; |
| for (std::string value : values) { |
| filter_values_msg.mutable_values()->Add(std::move(value)); |
| } |
| (*msg.mutable_filter_values())[filter] = std::move(filter_values_msg); |
| } |
| |
| std::string string; |
| bool success = msg.SerializeToString(&string); |
| CHECK(success); |
| return string; |
| } |
| |
| std::string SerializeReportMetadata( |
| const AttributionEventLevelMetadataRecord& record) { |
| proto::AttributionEventLevelMetadata msg; |
| |
| if (record.trigger_data) { |
| msg.set_trigger_data(*record.trigger_data); |
| } |
| if (record.priority) { |
| msg.set_priority(*record.priority); |
| } |
| |
| std::string str; |
| bool success = msg.SerializeToString(&str); |
| CHECK(success); |
| return str; |
| } |
| |
| std::string SerializeReportMetadata( |
| const AttributionAggregatableMetadataRecord& record) { |
| proto::AttributionAggregatableMetadata msg; |
| |
| if (record.coordinator) { |
| msg.mutable_common_data()->set_coordinator( |
| static_cast<proto::AttributionCommonAggregatableMetadata_Coordinator>( |
| *record.coordinator)); |
| } |
| |
| for (const auto& contribution : record.contributions) { |
| proto::AttributionAggregatableMetadata_Contribution* contribution_msg = |
| msg.add_contributions(); |
| if (contribution.high_bits) { |
| contribution_msg->mutable_key()->set_high_bits(*contribution.high_bits); |
| } |
| if (contribution.low_bits) { |
| contribution_msg->mutable_key()->set_low_bits(*contribution.low_bits); |
| } |
| if (contribution.value) { |
| contribution_msg->set_value(*contribution.value); |
| } |
| } |
| |
| if (record.source_registration_time_config) { |
| msg.mutable_common_data()->set_source_registration_time_config( |
| *record.source_registration_time_config); |
| } |
| |
| std::string str; |
| bool success = msg.SerializeToString(&str); |
| CHECK(success); |
| return str; |
| } |
| |
| std::string SerializeReportMetadata( |
| const AttributionNullAggregatableMetadataRecord& record) { |
| proto::AttributionNullAggregatableMetadata msg; |
| |
| if (record.coordinator) { |
| msg.mutable_common_data()->set_coordinator( |
| static_cast<proto::AttributionCommonAggregatableMetadata_Coordinator>( |
| *record.coordinator)); |
| } |
| |
| if (record.fake_source_time) { |
| msg.set_fake_source_time(*record.fake_source_time); |
| } |
| |
| if (record.source_registration_time_config) { |
| msg.mutable_common_data()->set_source_registration_time_config( |
| *record.source_registration_time_config); |
| } |
| |
| std::string str; |
| bool success = msg.SerializeToString(&str); |
| CHECK(success); |
| return str; |
| } |
| |
| class AttributionStorageSqlTest : public testing::Test { |
| public: |
| AttributionStorageSqlTest() = default; |
| |
| void SetUp() override { ASSERT_TRUE(temp_directory_.CreateUniqueTempDir()); } |
| |
| void OpenDatabase() { |
| CloseDatabase(); |
| auto delegate = std::make_unique<ConfigurableStorageDelegate>(); |
| delegate_ = delegate.get(); |
| storage_ = std::make_unique<AttributionStorageSql>( |
| temp_directory_.GetPath(), std::move(delegate)); |
| } |
| |
| void CloseDatabase() { |
| delegate_ = nullptr; |
| storage_.reset(); |
| } |
| |
| void AddReportToStorage() { |
| storage_->StoreSource(SourceBuilder().Build()); |
| storage_->MaybeCreateAndStoreReport(DefaultTrigger()); |
| } |
| |
| void ExpectAllTablesEmpty() { |
| sql::Database raw_db; |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| |
| static constexpr const char* kTables[] = { |
| "sources", "reports", "source_destinations", |
| "rate_limits", "dedup_keys", |
| }; |
| |
| for (const char* table : kTables) { |
| size_t rows; |
| sql::test::CountTableRows(&raw_db, table, &rows); |
| EXPECT_EQ(0u, rows) << table; |
| } |
| } |
| |
| base::FilePath db_path() { |
| return temp_directory_.GetPath().Append(FILE_PATH_LITERAL("Conversions")); |
| } |
| |
| AttributionStorage* storage() { return storage_.get(); } |
| |
| ConfigurableStorageDelegate* delegate() { return delegate_; } |
| |
| void ExpectImpressionRows(size_t expected) { |
| sql::Database raw_db; |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| size_t rows; |
| sql::test::CountTableRows(&raw_db, "sources", &rows); |
| EXPECT_EQ(expected, rows); |
| } |
| |
| AttributionTrigger::EventLevelResult MaybeCreateAndStoreEventLevelReport( |
| const AttributionTrigger& conversion) { |
| return storage_->MaybeCreateAndStoreReport(conversion).event_level_status(); |
| } |
| |
| void StoreAttributionReport(const AttributionReportRecord& record) { |
| sql::Database raw_db; |
| ASSERT_TRUE(raw_db.Open(db_path())); |
| |
| static constexpr char kStoreReportSql[] = |
| "INSERT INTO reports " |
| "VALUES(?,?,?,?,?,?,?,?,?,?,?,?)"; |
| sql::Statement statement(raw_db.GetUniqueStatement(kStoreReportSql)); |
| statement.BindInt64(0, record.report_id); |
| statement.BindInt64(1, record.source_id); |
| statement.BindTime(2, record.trigger_time); |
| statement.BindTime(3, record.report_time); |
| statement.BindTime(4, record.initial_report_time); |
| statement.BindInt(5, record.failed_send_attempts); |
| statement.BindString(6, record.external_report_id); |
| |
| if (record.debug_key) { |
| statement.BindInt64(7, *record.debug_key); |
| } else { |
| statement.BindNull(7); |
| } |
| statement.BindString(8, record.context_origin); |
| statement.BindString(9, record.reporting_origin); |
| |
| statement.BindInt(10, record.report_type); |
| statement.BindBlob(11, record.metadata); |
| ASSERT_TRUE(statement.Run()); |
| } |
| |
| protected: |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| base::ScopedTempDir temp_directory_; |
| |
| private: |
| std::unique_ptr<AttributionStorage> storage_; |
| raw_ptr<ConfigurableStorageDelegate> delegate_ = nullptr; |
| }; |
| |
| TEST_F(AttributionStorageSqlTest, |
| DatabaseInitialized_TablesAndIndexesLazilyInitialized) { |
| base::HistogramTester histograms; |
| |
| OpenDatabase(); |
| CloseDatabase(); |
| |
| // An unused AttributionStorageSql instance should not create the database. |
| EXPECT_FALSE(base::PathExists(db_path())); |
| |
| // Operations which don't need to run on an empty database should not create |
| // the database. |
| OpenDatabase(); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), IsEmpty()); |
| CloseDatabase(); |
| |
| EXPECT_FALSE(base::PathExists(db_path())); |
| |
| // DB init UMA should not be recorded. |
| histograms.ExpectTotalCount("Conversions.Storage.CreationTime", 0); |
| histograms.ExpectTotalCount("Conversions.Storage.MigrationTime", 0); |
| |
| // Storing an impression should create and initialize the database. |
| OpenDatabase(); |
| storage()->StoreSource(SourceBuilder().Build()); |
| CloseDatabase(); |
| |
| // DB creation histograms should be recorded. |
| histograms.ExpectTotalCount("Conversions.Storage.CreationTime", 1); |
| histograms.ExpectTotalCount("Conversions.Storage.MigrationTime", 0); |
| |
| { |
| sql::Database raw_db; |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| |
| // [sources], [reports], [meta], [rate_limits], [dedup_keys], |
| // [source_destinations], [sqlite_sequence] (for AUTOINCREMENT support). |
| EXPECT_EQ(7u, sql::test::CountSQLTables(&raw_db)); |
| |
| // [conversion_domain_idx], [impression_expiry_idx], |
| // [impression_origin_idx], [sources_by_source_time], |
| // [reports_by_report_time], [reports_by_source_id_report_type], |
| // [reports_by_trigger_time], [reports_by_reporting_origin], |
| // [rate_limit_source_site_reporting_origin_idx], |
| // [rate_limit_reporting_origin_idx], [rate_limit_time_idx], |
| // [rate_limit_impression_id_idx], [sources_by_destination_site], and the |
| // meta table index. |
| EXPECT_EQ(14u, sql::test::CountSQLIndices(&raw_db)); |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, DatabaseReopened_DataPersisted) { |
| OpenDatabase(); |
| AddReportToStorage(); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), SizeIs(1)); |
| CloseDatabase(); |
| OpenDatabase(); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), SizeIs(1)); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, CorruptDatabase_RecoveredOnOpen) { |
| OpenDatabase(); |
| AddReportToStorage(); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), SizeIs(1)); |
| CloseDatabase(); |
| |
| // Corrupt the database. |
| EXPECT_TRUE(sql::test::CorruptSizeInHeader(db_path())); |
| |
| sql::test::ScopedErrorExpecter expecter; |
| expecter.ExpectError(SQLITE_CORRUPT); |
| |
| // Open that database and ensure that it does not fail. |
| EXPECT_NO_FATAL_FAILURE(OpenDatabase()); |
| |
| // TODO(crbug.com/1418026): The recovery process does not recover tables |
| // without row IDs, causing no data to be returned here. Data recovery should |
| // be addressed in a separate CL. |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), SizeIs(0)); |
| |
| EXPECT_TRUE(expecter.SawExpectedErrors()); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, VersionTooNew_RazesDB) { |
| OpenDatabase(); |
| AddReportToStorage(); |
| ASSERT_THAT(storage()->GetAttributionReports(base::Time::Now()), SizeIs(1)); |
| CloseDatabase(); |
| |
| { |
| sql::Database raw_db; |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| |
| sql::MetaTable meta; |
| // The values here are irrelevant, as the meta table already exists. |
| ASSERT_TRUE(meta.Init(&raw_db, /*version=*/1, /*compatible_version=*/1)); |
| |
| ASSERT_TRUE(meta.SetVersionNumber(meta.GetVersionNumber() + 1)); |
| ASSERT_TRUE(meta.SetCompatibleVersionNumber(meta.GetVersionNumber() + 1)); |
| } |
| |
| // The DB should be razed because the version is too new. |
| ASSERT_NO_FATAL_FAILURE(OpenDatabase()); |
| ASSERT_THAT(storage()->GetAttributionReports(base::Time::Now()), IsEmpty()); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| StoreAndRetrieveReportWithVerification_FeatureEnabled) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| network::features::kAttributionReportingReportVerification); |
| base::HistogramTester histograms; |
| |
| OpenDatabase(); |
| |
| StorableSource source = TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetExpiry(base::Days(30)) |
| .Build(); |
| storage()->StoreSource(source); |
| |
| auto trigger_verification = network::TriggerVerification::Create( |
| /*token=*/"verification-token", /*aggregatable_report_id=*/ |
| "55865da3-fb0e-4b71-965e-64fc4bf0a323"); |
| AttributionTrigger trigger = DefaultAggregatableTriggerBuilder() |
| .SetVerification(trigger_verification) |
| .Build(); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(trigger), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportVerification.ReportHasVerification", true, |
| /*expected_bucket_count=*/1); |
| |
| AttributionReport aggregatable_report = |
| storage()->GetAttributionReports(base::Time::Max()).at(1); |
| // Should create the report with the id from the trigger verification. |
| EXPECT_EQ(aggregatable_report.external_report_id(), |
| trigger_verification->aggregatable_report_id()); |
| |
| // Should store the verification token on the report. |
| const auto* data = |
| absl::get_if<AttributionReport::AggregatableAttributionData>( |
| &aggregatable_report.data()); |
| EXPECT_EQ(data->common_data.verification_token.value(), |
| trigger_verification->token()); |
| |
| CloseDatabase(); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| StoreAndRetrieveReportWithoutVerification_FeatureEnabled) { |
| OpenDatabase(); |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| network::features::kAttributionReportingReportVerification); |
| base::HistogramTester histograms; |
| |
| StorableSource source = TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetExpiry(base::Days(30)) |
| .Build(); |
| storage()->StoreSource(source); |
| AttributionTrigger trigger = DefaultAggregatableTriggerBuilder().Build(); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(trigger), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportVerification.ReportHasVerification", false, |
| /*expected_bucket_count=*/1); |
| |
| AttributionReport aggregatable_report = |
| storage()->GetAttributionReports(base::Time::Max()).at(1); |
| |
| const auto* data = |
| absl::get_if<AttributionReport::AggregatableAttributionData>( |
| &aggregatable_report.data()); |
| EXPECT_FALSE(data->common_data.verification_token.has_value()); |
| |
| CloseDatabase(); |
| } |
| |
| TEST_F( |
| AttributionStorageSqlTest, |
| StoreAndRetrieveReportWithoutVerification_FeatureDisabled_HasVerificationNotRecorded) { |
| OpenDatabase(); |
| |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndDisableFeature( |
| network::features::kAttributionReportingReportVerification); |
| base::HistogramTester histograms; |
| |
| StorableSource source = TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetExpiry(base::Days(30)) |
| .Build(); |
| storage()->StoreSource(source); |
| AttributionTrigger trigger = DefaultAggregatableTriggerBuilder().Build(); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport(trigger), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportVerification.ReportHasVerification", false, |
| /*expected_bucket_count=*/0); |
| |
| AttributionReport aggregatable_report = |
| storage()->GetAttributionReports(base::Time::Max()).at(1); |
| |
| const auto* data = |
| absl::get_if<AttributionReport::AggregatableAttributionData>( |
| &aggregatable_report.data()); |
| EXPECT_FALSE(data->common_data.verification_token.has_value()); |
| |
| CloseDatabase(); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, NullReportWithVerification_FeatureEnabled) { |
| OpenDatabase(); |
| |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| network::features::kAttributionReportingReportVerification); |
| base::HistogramTester histograms; |
| |
| delegate()->set_null_aggregatable_reports({ |
| AttributionStorageDelegate::NullAggregatableReport{ |
| .fake_source_time = base::Time::Now(), |
| }, |
| AttributionStorageDelegate::NullAggregatableReport{ |
| .fake_source_time = base::Time::Now() - base::Days(1), |
| }, |
| }); |
| auto trigger_verification = network::TriggerVerification::Create( |
| /*token=*/"verification-token", /*aggregatable_report_id=*/ |
| "55865da3-fb0e-4b71-965e-64fc4bf0a323"); |
| AttributionTrigger trigger = DefaultAggregatableTriggerBuilder() |
| .SetVerification(trigger_verification) |
| .Build(); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| EXPECT_TRUE(result.min_null_aggregatable_report_time().has_value()); |
| |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportVerification.ReportHasVerification", true, |
| /*expected_bucket_count=*/1); |
| |
| auto reports = storage()->GetAttributionReports(base::Time::Max()); |
| ASSERT_THAT(reports, SizeIs(2)); |
| base::ranges::sort(reports, std::less<>(), &AttributionReport::id); |
| |
| // Only the first report was created with the id from the trigger |
| // verification. |
| const AttributionReport& first_report = reports.front(); |
| EXPECT_EQ(first_report.external_report_id(), |
| trigger_verification->aggregatable_report_id()); |
| // Should store the verification token on the report. |
| const auto* data = absl::get_if<AttributionReport::NullAggregatableData>( |
| &first_report.data()); |
| EXPECT_EQ(data->common_data.verification_token.value(), |
| trigger_verification->token()); |
| |
| // The second report was not created with the trigger verification. |
| const AttributionReport& second_report = reports.back(); |
| EXPECT_EQ(second_report.external_report_id(), DefaultExternalReportID()); |
| data = absl::get_if<AttributionReport::NullAggregatableData>( |
| &second_report.data()); |
| EXPECT_FALSE(data->common_data.verification_token.has_value()); |
| CloseDatabase(); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| BothRealAndNullReports_OnlyOneReportWithVerification) { |
| OpenDatabase(); |
| |
| StorableSource source = TestAggregatableSourceProvider().GetBuilder().Build(); |
| storage()->StoreSource(source); |
| |
| delegate()->set_null_aggregatable_reports({ |
| AttributionStorageDelegate::NullAggregatableReport{ |
| .fake_source_time = base::Time::Now(), |
| }, |
| }); |
| auto trigger_verification = network::TriggerVerification::Create( |
| /*token=*/"verification-token", /*aggregatable_report_id=*/ |
| "55865da3-fb0e-4b71-965e-64fc4bf0a323"); |
| AttributionTrigger trigger = |
| DefaultAggregatableTriggerBuilder() |
| .SetVerification(trigger_verification) |
| .Build(/*generate_event_trigger_data=*/false); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| |
| EXPECT_EQ(result.aggregatable_status(), |
| AttributionTrigger::AggregatableResult::kSuccess); |
| EXPECT_TRUE(result.min_null_aggregatable_report_time().has_value()); |
| |
| auto reports = storage()->GetAttributionReports(base::Time::Max()); |
| ASSERT_THAT(reports, SizeIs(2)); |
| base::ranges::sort(reports, std::less<>(), &AttributionReport::id); |
| |
| // Only the first report was created with the id from the trigger |
| // verification. |
| const AttributionReport& first_report = reports.front(); |
| EXPECT_EQ(first_report.external_report_id(), |
| trigger_verification->aggregatable_report_id()); |
| // Should store the verification token on the report. |
| const auto* aggregatable_data = |
| absl::get_if<AttributionReport::AggregatableAttributionData>( |
| &first_report.data()); |
| EXPECT_EQ(aggregatable_data->common_data.verification_token.value(), |
| trigger_verification->token()); |
| |
| // The second report was not created with the trigger verification. |
| const AttributionReport& second_report = reports.back(); |
| EXPECT_EQ(second_report.external_report_id(), DefaultExternalReportID()); |
| const auto* null_data = absl::get_if<AttributionReport::NullAggregatableData>( |
| &second_report.data()); |
| EXPECT_FALSE(null_data->common_data.verification_token.has_value()); |
| CloseDatabase(); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| BothRealAndNullReportsReverseShuffle_OnlyOneReportWithVerification) { |
| OpenDatabase(); |
| |
| StorableSource source = TestAggregatableSourceProvider().GetBuilder().Build(); |
| storage()->StoreSource(source); |
| |
| delegate()->set_null_aggregatable_reports({ |
| AttributionStorageDelegate::NullAggregatableReport{ |
| .fake_source_time = base::Time::Now(), |
| }, |
| }); |
| delegate()->set_reverse_reports_on_shuffle(true); |
| auto trigger_verification = network::TriggerVerification::Create( |
| /*token=*/"verification-token", /*aggregatable_report_id=*/ |
| "55865da3-fb0e-4b71-965e-64fc4bf0a323"); |
| AttributionTrigger trigger = |
| DefaultAggregatableTriggerBuilder() |
| .SetVerification(trigger_verification) |
| .Build(/*generate_event_trigger_data=*/false); |
| auto result = storage()->MaybeCreateAndStoreReport(trigger); |
| |
| EXPECT_EQ(result.aggregatable_status(), |
| AttributionTrigger::AggregatableResult::kSuccess); |
| EXPECT_TRUE(result.min_null_aggregatable_report_time().has_value()); |
| |
| auto reports = storage()->GetAttributionReports(base::Time::Max()); |
| ASSERT_THAT(reports, SizeIs(2)); |
| base::ranges::sort(reports, std::less<>(), &AttributionReport::id); |
| |
| // Only the first report was created with the id from the trigger |
| // verification. |
| const AttributionReport& first_report = reports.front(); |
| EXPECT_EQ(first_report.external_report_id(), |
| trigger_verification->aggregatable_report_id()); |
| // Should store the verification token on the report. |
| const auto* null_data = absl::get_if<AttributionReport::NullAggregatableData>( |
| &first_report.data()); |
| EXPECT_EQ(null_data->common_data.verification_token.value(), |
| trigger_verification->token()); |
| |
| // The second report was not created with the trigger verification. |
| const AttributionReport& second_report = reports.back(); |
| EXPECT_EQ(second_report.external_report_id(), DefaultExternalReportID()); |
| const auto* aggregatable_data = |
| absl::get_if<AttributionReport::AggregatableAttributionData>( |
| &second_report.data()); |
| EXPECT_FALSE(aggregatable_data->common_data.verification_token.has_value()); |
| CloseDatabase(); |
| } |
| |
| // Create a source with three triggers and craft a query that will target all. |
| TEST_F(AttributionStorageSqlTest, ClearDataRangeMultipleReports) { |
| base::HistogramTester histograms; |
| |
| OpenDatabase(); |
| |
| base::Time start = base::Time::Now(); |
| auto source = TestAggregatableSourceProvider() |
| .GetBuilder(start) |
| .SetExpiry(base::Days(30)) |
| .Build(); |
| storage()->StoreSource(source); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| // Use a time range that targets all triggers. |
| storage()->ClearData( |
| base::Time::Min(), base::Time::Max(), |
| base::BindRepeating(std::equal_to<blink::StorageKey>(), |
| blink::StorageKey::CreateFirstParty( |
| source.common_info().reporting_origin()))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| |
| CloseDatabase(); |
| |
| // Verify that everything is deleted. |
| ExpectAllTablesEmpty(); |
| |
| histograms.ExpectUniqueSample( |
| "Conversions.ImpressionsDeletedInDataClearOperation", 1, 1); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportsDeletedInDataClearOperation.Event", 3, 1); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportsDeletedInDataClearOperation.Aggregatable", 3, 1); |
| } |
| |
| // Create a source with two triggers resulting in two event-level reports (C1 |
| // and C2) and two aggregatable reports (A1 and A2). Craft a query that will |
| // target C2 and A2, which will in turn delete the source. We should ensure |
| // that C1 and A1 are properly deleted (reports should not be stored |
| // unattributed). |
| TEST_F(AttributionStorageSqlTest, ClearDataWithVestigialConversion) { |
| base::HistogramTester histograms; |
| |
| OpenDatabase(); |
| |
| base::Time start = base::Time::Now(); |
| auto source = TestAggregatableSourceProvider() |
| .GetBuilder(start) |
| .SetExpiry(base::Days(30)) |
| .Build(); |
| storage()->StoreSource(source); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| // Use a time range that only intersects the last trigger. |
| storage()->ClearData( |
| base::Time::Now(), base::Time::Now(), |
| base::BindRepeating(std::equal_to<blink::StorageKey>(), |
| blink::StorageKey::CreateFirstParty( |
| source.common_info().reporting_origin()))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| |
| CloseDatabase(); |
| |
| // Verify that everything is deleted. |
| ExpectAllTablesEmpty(); |
| |
| histograms.ExpectUniqueSample( |
| "Conversions.ImpressionsDeletedInDataClearOperation", 1, 1); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportsDeletedInDataClearOperation.Event", 2, 1); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportsDeletedInDataClearOperation.Aggregatable", 2, 1); |
| } |
| |
| // Same as the above test, but with a null filter. |
| TEST_F(AttributionStorageSqlTest, ClearAllDataWithVestigialConversion) { |
| base::HistogramTester histograms; |
| |
| OpenDatabase(); |
| |
| base::Time start = base::Time::Now(); |
| auto source = TestAggregatableSourceProvider() |
| .GetBuilder(start) |
| .SetExpiry(base::Days(30)) |
| .Build(); |
| storage()->StoreSource(source); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| // Use a time range that only intersects the last trigger. |
| storage()->ClearData(base::Time::Now(), base::Time::Now(), |
| base::NullCallback()); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| |
| CloseDatabase(); |
| |
| // Verify that everything is deleted. |
| ExpectAllTablesEmpty(); |
| |
| histograms.ExpectUniqueSample( |
| "Conversions.ImpressionsDeletedInDataClearOperation", 1, 1); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportsDeletedInDataClearOperation.Event", 2, 1); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportsDeletedInDataClearOperation.Aggregatable", 2, 1); |
| } |
| |
| // The max time range with a null filter should delete everything. |
| TEST_F(AttributionStorageSqlTest, DeleteEverything) { |
| base::HistogramTester histograms; |
| |
| OpenDatabase(); |
| |
| base::Time start = base::Time::Now(); |
| for (int i = 0; i < 10; i++) { |
| auto source = TestAggregatableSourceProvider() |
| .GetBuilder(start) |
| .SetExpiry(base::Days(30)) |
| .Build(); |
| storage()->StoreSource(source); |
| task_environment_.FastForwardBy(base::Days(1)); |
| } |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| task_environment_.FastForwardBy(base::Days(1)); |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), IsEmpty()); |
| |
| CloseDatabase(); |
| |
| // Verify that everything is deleted. |
| ExpectAllTablesEmpty(); |
| |
| histograms.ExpectUniqueSample( |
| "Conversions.ImpressionsDeletedInDataClearOperation", 1, 1); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportsDeletedInDataClearOperation.Event", 2, 1); |
| histograms.ExpectUniqueSample( |
| "Conversions.ReportsDeletedInDataClearOperation.Aggregatable", 2, 1); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, ClearData_KeepRateLimitData) { |
| OpenDatabase(); |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| CloseDatabase(); |
| { |
| sql::Database raw_db; |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| size_t impression_rows; |
| sql::test::CountTableRows(&raw_db, "sources", &impression_rows); |
| EXPECT_EQ(1u, impression_rows); |
| |
| size_t rate_limit_rows; |
| sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows); |
| EXPECT_EQ(2u, rate_limit_rows); |
| } |
| |
| OpenDatabase(); |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback(), |
| /*delete_rate_limit_data=*/false); |
| CloseDatabase(); |
| |
| { |
| sql::Database raw_db; |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| size_t impression_rows; |
| sql::test::CountTableRows(&raw_db, "sources", &impression_rows); |
| EXPECT_EQ(0u, impression_rows); |
| |
| size_t rate_limit_rows; |
| sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows); |
| EXPECT_EQ(2u, rate_limit_rows); |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, DeleteAttributionDataByDataKey) { |
| OpenDatabase(); |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetReportingOrigin( |
| *attribution_reporting::SuitableOrigin::Deserialize( |
| "https://report1.test")) |
| .Build()); |
| |
| delegate()->set_null_aggregatable_reports( |
| {AttributionStorageDelegate::NullAggregatableReport{ |
| .fake_source_time = base::Time::Now(), |
| }}); |
| AttributionTrigger trigger = |
| DefaultAggregatableTriggerBuilder() |
| .SetReportingOrigin( |
| *attribution_reporting::SuitableOrigin::Deserialize( |
| "https://report2.test")) |
| .Build(); |
| storage()->MaybeCreateAndStoreReport(trigger); |
| |
| std::vector keys = storage()->GetAllDataKeys(); |
| ASSERT_THAT(keys, SizeIs(2)); |
| |
| storage()->DeleteByDataKey(keys[0]); |
| storage()->DeleteByDataKey(keys[1]); |
| |
| CloseDatabase(); |
| |
| sql::Database raw_db; |
| ASSERT_TRUE(raw_db.Open(db_path())); |
| { |
| sql::Statement s(raw_db.GetUniqueStatement("SELECT * FROM reports")); |
| ASSERT_FALSE(s.Step()); |
| } |
| { |
| sql::Statement s(raw_db.GetUniqueStatement("SELECT * FROM sources")); |
| ASSERT_FALSE(s.Step()); |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, MaxSourcesPerOrigin) { |
| OpenDatabase(); |
| delegate()->set_max_sources_per_origin(2); |
| storage()->StoreSource(SourceBuilder().Build()); |
| storage()->StoreSource(SourceBuilder().Build()); |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| CloseDatabase(); |
| sql::Database raw_db; |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| size_t impression_rows; |
| sql::test::CountTableRows(&raw_db, "sources", &impression_rows); |
| EXPECT_EQ(1u, impression_rows); |
| size_t rate_limit_rows; |
| sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows); |
| EXPECT_EQ(3u, rate_limit_rows); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, MaxReportsPerDestination) { |
| OpenDatabase(); |
| delegate()->set_max_reports_per_destination( |
| AttributionReport::Type::kEventLevel, 2); |
| storage()->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| EXPECT_EQ( |
| AttributionTrigger::EventLevelResult::kNoCapacityForConversionDestination, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| CloseDatabase(); |
| sql::Database raw_db; |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| size_t conversion_rows; |
| sql::test::CountTableRows(&raw_db, "reports", &conversion_rows); |
| EXPECT_EQ(2u, conversion_rows); |
| size_t rate_limit_rows; |
| sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows); |
| EXPECT_EQ(3u, rate_limit_rows); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, CantOpenDb_FailsSilentlyInRelease) { |
| base::CreateDirectoryAndGetError(db_path(), nullptr); |
| |
| auto sql_storage = std::make_unique<AttributionStorageSql>( |
| temp_directory_.GetPath(), |
| std::make_unique<ConfigurableStorageDelegate>()); |
| sql_storage->set_ignore_errors_for_testing(true); |
| |
| std::unique_ptr<AttributionStorage> storage = std::move(sql_storage); |
| |
| // These calls should be no-ops. |
| storage->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kInternalError, |
| storage->MaybeCreateAndStoreReport(DefaultTrigger()) |
| .event_level_status()); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, DatabaseDirDoesExist_CreateDirAndOpenDB) { |
| // Give the storage layer a database directory that doesn't exist. |
| std::unique_ptr<AttributionStorage> storage = |
| std::make_unique<AttributionStorageSql>( |
| temp_directory_.GetPath().Append( |
| FILE_PATH_LITERAL("ConversionFolder/")), |
| std::make_unique<ConfigurableStorageDelegate>()); |
| |
| // The directory should be created, and the database opened. |
| storage->StoreSource(SourceBuilder().Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| storage->MaybeCreateAndStoreReport(DefaultTrigger()) |
| .event_level_status()); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, DBinitializationSucceeds_HistogramRecorded) { |
| base::HistogramTester histograms; |
| |
| OpenDatabase(); |
| storage()->StoreSource(SourceBuilder().Build()); |
| CloseDatabase(); |
| |
| histograms.ExpectUniqueSample("Conversions.Storage.Sql.InitStatus2", |
| AttributionStorageSql::InitStatus::kSuccess, 1); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, MaxUint64StorageSucceeds) { |
| constexpr uint64_t kMaxUint64 = std::numeric_limits<uint64_t>::max(); |
| |
| OpenDatabase(); |
| |
| // Ensure that reading and writing `uint64_t` fields via |
| // `sql::Statement::ColumnInt64()` and `sql::Statement::BindInt64()` works |
| // with the maximum value. |
| |
| const auto impression = SourceBuilder().SetSourceEventId(kMaxUint64).Build(); |
| storage()->StoreSource(impression); |
| EXPECT_THAT(storage()->GetActiveSources(), |
| ElementsAre(SourceEventIdIs(kMaxUint64))); |
| |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport( |
| TriggerBuilder().SetDebugKey(kMaxUint64).Build())); |
| |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()), |
| ElementsAre(TriggerDebugKeyIs(kMaxUint64))); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, ImpressionNotExpired_NotDeleted) { |
| OpenDatabase(); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| // Store another impression to trigger the expiry logic. |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| |
| CloseDatabase(); |
| ExpectImpressionRows(2u); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, ImpressionExpired_Deleted) { |
| OpenDatabase(); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| // Store another impression to trigger the expiry logic. |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| |
| CloseDatabase(); |
| ExpectImpressionRows(1u); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, ImpressionExpired_TooFrequent_NotDeleted) { |
| OpenDatabase(); |
| |
| delegate()->set_delete_expired_sources_frequency(base::Milliseconds(4)); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| // Store another impression to trigger the expiry logic. |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| |
| CloseDatabase(); |
| ExpectImpressionRows(2u); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| ExpiredImpressionWithPendingConversion_NotDeleted) { |
| OpenDatabase(); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| // Store another impression to trigger the expiry logic. |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| |
| CloseDatabase(); |
| ExpectImpressionRows(2u); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, TwoImpressionsOneExpired_OneDeleted) { |
| OpenDatabase(); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(4)).Build()); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| // Store another impression to trigger the expiry logic. |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| |
| CloseDatabase(); |
| ExpectImpressionRows(2u); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, ExpiredImpressionWithSentConversion_Deleted) { |
| OpenDatabase(); |
| |
| const base::TimeDelta kReportDelay = base::Milliseconds(5); |
| delegate()->set_report_delay(kReportDelay); |
| |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess, |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger())); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| // Advance past the default report time. |
| task_environment_.FastForwardBy(kReportDelay); |
| |
| std::vector<AttributionReport> reports = |
| storage()->GetAttributionReports(base::Time::Now()); |
| EXPECT_THAT(reports, SizeIs(1)); |
| EXPECT_TRUE(storage()->DeleteReport(reports[0].id())); |
| // Store another impression to trigger the expiry logic. |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| |
| CloseDatabase(); |
| ExpectImpressionRows(1u); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, DeleteAggregatableAttributionReport) { |
| OpenDatabase(); |
| |
| storage()->StoreSource(TestAggregatableSourceProvider().GetBuilder().Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| std::vector<AttributionReport> reports = |
| storage()->GetAttributionReports(base::Time::Max()); |
| |
| EXPECT_THAT( |
| reports, |
| ElementsAre( |
| ReportTypeIs(AttributionReport::Type::kEventLevel), |
| ReportTypeIs(AttributionReport::Type::kAggregatableAttribution))); |
| |
| EXPECT_TRUE(storage()->DeleteReport(AttributionReport::Id(2))); |
| EXPECT_THAT(storage()->GetAttributionReports(base::Time::Max()), |
| ElementsAre(ReportTypeIs(AttributionReport::Type::kEventLevel))); |
| |
| CloseDatabase(); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| ExpiredSourceWithPendingAggregatableAttribution_NotDeleted) { |
| OpenDatabase(); |
| |
| storage()->StoreSource(TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetExpiry(base::Milliseconds(3)) |
| .Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| std::vector<AttributionReport> reports = |
| storage()->GetAttributionReports(base::Time::Max()); |
| |
| EXPECT_THAT( |
| reports, |
| ElementsAre( |
| ReportTypeIs(AttributionReport::Type::kEventLevel), |
| ReportTypeIs(AttributionReport::Type::kAggregatableAttribution))); |
| |
| EXPECT_TRUE(storage()->DeleteReport(AttributionReport::Id(1))); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| // Store another source to trigger the expiry logic. |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| |
| CloseDatabase(); |
| ExpectImpressionRows(2u); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| ExpiredSourceWithSentAggregatableAttribution_Deleted) { |
| OpenDatabase(); |
| |
| storage()->StoreSource(TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetExpiry(base::Milliseconds(3)) |
| .Build()); |
| |
| EXPECT_THAT(storage()->MaybeCreateAndStoreReport( |
| DefaultAggregatableTriggerBuilder().Build()), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| std::vector<AttributionReport> reports = |
| storage()->GetAttributionReports(base::Time::Max()); |
| |
| EXPECT_THAT( |
| reports, |
| ElementsAre( |
| ReportTypeIs(AttributionReport::Type::kEventLevel), |
| ReportTypeIs(AttributionReport::Type::kAggregatableAttribution))); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(3)); |
| |
| EXPECT_TRUE(storage()->DeleteReport(reports[0].id())); |
| EXPECT_TRUE(storage()->DeleteReport(reports[1].id())); |
| |
| // Store another source to trigger the expiry logic. |
| storage()->StoreSource( |
| SourceBuilder().SetExpiry(base::Milliseconds(3)).Build()); |
| |
| CloseDatabase(); |
| ExpectImpressionRows(1u); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| InvalidSourceOriginOrSite_FailsDeserialization) { |
| const struct { |
| const char* sql; |
| const char* value; |
| } kTestCases[] = { |
| { |
| .sql = "UPDATE sources SET source_origin=?", |
| .value = "http://insecure.test", |
| }, |
| { |
| .sql = "UPDATE sources SET reporting_origin=?", |
| .value = "http://insecure.test", |
| }, |
| { |
| .sql = "UPDATE source_destinations SET destination_site=?", |
| .value = "wss://a.test", |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| OpenDatabase(); |
| |
| SourceBuilder source_builder; |
| storage()->StoreSource( |
| source_builder.SetExpiry(base::Milliseconds(3)).Build()); |
| ASSERT_THAT(storage()->GetActiveSources(), SizeIs(1)) << test_case.sql; |
| |
| CloseDatabase(); |
| |
| { |
| sql::Database raw_db; |
| ASSERT_TRUE(raw_db.Open(db_path())) << test_case.sql; |
| |
| sql::Statement statement(raw_db.GetUniqueStatement(test_case.sql)); |
| statement.BindString(0, test_case.value); |
| ASSERT_TRUE(statement.Run()) << test_case.sql; |
| } |
| |
| OpenDatabase(); |
| ASSERT_THAT(storage()->GetActiveSources(), IsEmpty()) << test_case.sql; |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| CloseDatabase(); |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, CreateReport_DeletesUnattributedSources) { |
| OpenDatabase(); |
| storage()->StoreSource(SourceBuilder().Build()); |
| storage()->StoreSource(SourceBuilder().Build()); |
| CloseDatabase(); |
| |
| ExpectImpressionRows(2); |
| |
| OpenDatabase(); |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger()); |
| CloseDatabase(); |
| |
| ExpectImpressionRows(1); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, CreateReport_DeactivatesAttributedSources) { |
| OpenDatabase(); |
| storage()->StoreSource( |
| SourceBuilder().SetSourceEventId(1).SetPriority(1).Build()); |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger()); |
| storage()->StoreSource( |
| SourceBuilder().SetSourceEventId(2).SetPriority(2).Build()); |
| MaybeCreateAndStoreEventLevelReport(DefaultTrigger()); |
| CloseDatabase(); |
| |
| ExpectImpressionRows(2); |
| } |
| |
| // Tests that a "source_type" filter present in the serialized data is |
| // removed. |
| TEST_F(AttributionStorageSqlTest, |
| DeserializeFilterData_RemovesSourceTypeFilter) { |
| { |
| OpenDatabase(); |
| storage()->StoreSource(SourceBuilder().Build()); |
| CloseDatabase(); |
| } |
| |
| { |
| sql::Database raw_db; |
| ASSERT_TRUE(raw_db.Open(db_path())); |
| |
| static constexpr char kUpdateSql[] = "UPDATE sources SET filter_data=?"; |
| sql::Statement statement(raw_db.GetUniqueStatement(kUpdateSql)); |
| statement.BindBlob(0, CreateSerializedFilterData( |
| {{"source_type", {"abc"}}, {"x", {"y"}}})); |
| ASSERT_TRUE(statement.Run()); |
| } |
| |
| OpenDatabase(); |
| |
| std::vector<StoredSource> sources = storage()->GetActiveSources(); |
| ASSERT_EQ(sources.size(), 1u); |
| ASSERT_THAT(sources.front().filter_data().filter_values(), |
| ElementsAre(Pair("x", ElementsAre("y")))); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, ReportTablesStoreDestinationOrigin) { |
| constexpr char kDestinationOriginA[] = "https://a.d.test"; |
| constexpr char kDestinationOriginB[] = "https://b.d.test"; |
| |
| OpenDatabase(); |
| |
| StorableSource source = |
| TestAggregatableSourceProvider() |
| .GetBuilder() |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize(kDestinationOriginA)}) |
| .SetExpiry(base::Days(30)) |
| .Build(); |
| storage()->StoreSource(source); |
| |
| AttributionTrigger trigger = |
| DefaultAggregatableTriggerBuilder() |
| .SetDestinationOrigin( |
| *SuitableOrigin::Deserialize(kDestinationOriginB)) |
| .Build(); |
| ASSERT_THAT(storage()->MaybeCreateAndStoreReport(trigger), |
| AllOf(CreateReportEventLevelStatusIs( |
| AttributionTrigger::EventLevelResult::kSuccess), |
| CreateReportAggregatableStatusIs( |
| AttributionTrigger::AggregatableResult::kSuccess))); |
| |
| CloseDatabase(); |
| |
| sql::Database raw_db; |
| ASSERT_TRUE(raw_db.Open(db_path())); |
| |
| { |
| sql::Statement s( |
| raw_db.GetUniqueStatement("SELECT context_origin FROM reports")); |
| ASSERT_TRUE(s.Step()); |
| ASSERT_EQ(s.ColumnString(0), kDestinationOriginB); |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, FakeReportUsesSourceOriginAsContext) { |
| OpenDatabase(); |
| |
| delegate()->set_randomized_response( |
| std::vector<AttributionStorageDelegate::FakeReport>{ |
| {.trigger_data = 1, |
| .trigger_time = base::Time::Now() + base::Microseconds(1), |
| .report_time = base::Time::Now() + base::Microseconds(2)}}); |
| |
| storage()->StoreSource( |
| SourceBuilder() |
| .SetSourceOrigin(*SuitableOrigin::Deserialize("https://a.s.test")) |
| .SetDestinationSites( |
| {net::SchemefulSite::Deserialize("https://b.d.test")}) |
| .SetReportingOrigin(*SuitableOrigin::Deserialize("https://r.test")) |
| .Build()); |
| |
| CloseDatabase(); |
| |
| sql::Database raw_db; |
| ASSERT_TRUE(raw_db.Open(db_path())); |
| |
| { |
| sql::Statement s( |
| raw_db.GetUniqueStatement("SELECT context_origin FROM reports")); |
| ASSERT_TRUE(s.Step()); |
| ASSERT_EQ(s.ColumnString(0), "https://a.s.test"); |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, ReportTimes) { |
| OpenDatabase(); |
| |
| const attribution_reporting::DestinationSet destinations = |
| *attribution_reporting::DestinationSet::Create( |
| {net::SchemefulSite::Deserialize("https://dest.test")}); |
| |
| const auto reporting_origin = |
| *SuitableOrigin::Deserialize("https://report.test"); |
| |
| const base::Time kSourceTime = base::Time::Now(); |
| |
| const struct { |
| const char* desc; |
| absl::optional<base::TimeDelta> expiry; |
| absl::optional<base::TimeDelta> event_report_window; |
| absl::optional<base::TimeDelta> aggregatable_report_window; |
| base::Time expected_expiry_time; |
| base::Time expected_event_report_window_time; |
| base::Time expected_aggregatable_report_window_time; |
| } kTestCases[] = { |
| { |
| .desc = "expiry", |
| .expiry = base::Days(4), |
| .expected_expiry_time = kSourceTime + base::Days(4), |
| .expected_event_report_window_time = kSourceTime + base::Days(4), |
| .expected_aggregatable_report_window_time = |
| kSourceTime + base::Days(4), |
| }, |
| { |
| .desc = "event-report-window", |
| .event_report_window = base::Days(4), |
| .expected_expiry_time = kSourceTime + base::Days(30), |
| .expected_event_report_window_time = kSourceTime + base::Days(4), |
| .expected_aggregatable_report_window_time = |
| kSourceTime + base::Days(30), |
| }, |
| { |
| .desc = "clamp-event-report-window", |
| .expiry = base::Days(4), |
| .event_report_window = base::Days(30), |
| .expected_expiry_time = kSourceTime + base::Days(4), |
| .expected_event_report_window_time = kSourceTime + base::Days(4), |
| .expected_aggregatable_report_window_time = |
| kSourceTime + base::Days(4), |
| }, |
| { |
| .desc = "aggregatable-report-window", |
| .aggregatable_report_window = base::Days(4), |
| .expected_expiry_time = kSourceTime + base::Days(30), |
| .expected_event_report_window_time = kSourceTime + base::Days(30), |
| .expected_aggregatable_report_window_time = |
| kSourceTime + base::Days(4), |
| }, |
| { |
| .desc = "clamp-aggregatable-report-window", |
| .expiry = base::Days(4), |
| .aggregatable_report_window = base::Days(30), |
| .expected_expiry_time = kSourceTime + base::Days(4), |
| .expected_event_report_window_time = kSourceTime + base::Days(4), |
| .expected_aggregatable_report_window_time = |
| kSourceTime + base::Days(4), |
| }, |
| { |
| .desc = "all", |
| .expiry = base::Days(9), |
| .event_report_window = base::Days(7), |
| .aggregatable_report_window = base::Days(5), |
| .expected_expiry_time = kSourceTime + base::Days(9), |
| .expected_event_report_window_time = kSourceTime + base::Days(7), |
| .expected_aggregatable_report_window_time = |
| kSourceTime + base::Days(5), |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| attribution_reporting::SourceRegistration reg(destinations); |
| reg.expiry = test_case.expiry.value_or(base::Days(30)); |
| reg.event_report_window = test_case.event_report_window; |
| reg.aggregatable_report_window = test_case.aggregatable_report_window; |
| |
| storage()->StoreSource( |
| StorableSource(reporting_origin, std::move(reg), |
| *SuitableOrigin::Deserialize("https://source.test"), |
| attribution_reporting::mojom::SourceType::kNavigation, |
| /*is_within_fenced_frame=*/false)); |
| |
| std::vector<StoredSource> sources = storage()->GetActiveSources(); |
| ASSERT_THAT(sources, SizeIs(1)) << test_case.desc; |
| const StoredSource& actual = sources.front(); |
| |
| EXPECT_EQ(actual.expiry_time(), test_case.expected_expiry_time) |
| << test_case.desc; |
| |
| EXPECT_EQ(actual.event_report_window_time(), |
| test_case.expected_event_report_window_time) |
| << test_case.desc; |
| |
| EXPECT_EQ(actual.aggregatable_report_window_time(), |
| test_case.expected_aggregatable_report_window_time) |
| << test_case.desc; |
| |
| storage()->ClearData(/*delete_begin=*/base::Time::Min(), |
| /*delete_end=*/base::Time::Max(), |
| /*filter=*/base::NullCallback()); |
| } |
| |
| CloseDatabase(); |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| InvalidExpiryOrReportTime_FailsDeserialization) { |
| static constexpr const char* kUpdateSqls[] = { |
| "UPDATE sources SET expiry_time=?", |
| "UPDATE sources SET event_report_window_time=?", |
| "UPDATE sources SET aggregatable_report_window_time=?", |
| }; |
| |
| const struct { |
| base::TimeDelta time_from_source; |
| bool valid; |
| } kTestCases[] = { |
| { |
| base::TimeDelta(), |
| false, |
| }, |
| { |
| base::Milliseconds(1), |
| true, |
| }, |
| { |
| kDefaultAttributionSourceExpiry, |
| true, |
| }, |
| { |
| kDefaultAttributionSourceExpiry + base::Milliseconds(1), |
| false, |
| }, |
| }; |
| |
| for (const char* update_sql : kUpdateSqls) { |
| for (const auto& test_case : kTestCases) { |
| OpenDatabase(); |
| |
| base::Time now = base::Time::Now(); |
| storage()->StoreSource(SourceBuilder(now).Build()); |
| ASSERT_THAT(storage()->GetActiveSources(), SizeIs(1)) |
| << update_sql << "," << test_case.time_from_source; |
| |
| CloseDatabase(); |
| |
| { |
| sql::Database raw_db; |
| ASSERT_TRUE(raw_db.Open(db_path())) |
| << update_sql << "," << test_case.time_from_source; |
| |
| sql::Statement statement(raw_db.GetUniqueStatement(update_sql)); |
| statement.BindTime(0, now + test_case.time_from_source); |
| ASSERT_TRUE(statement.Run()) |
| << update_sql << "," << test_case.time_from_source; |
| } |
| |
| OpenDatabase(); |
| ASSERT_THAT(storage()->GetActiveSources(), SizeIs(test_case.valid)) |
| << update_sql << "," << test_case.time_from_source; |
| storage()->ClearData(/*delete_begin=*/base::Time::Min(), |
| /*delete_end=*/base::Time::Max(), |
| /*filter=*/base::NullCallback()); |
| CloseDatabase(); |
| } |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| InvalidEventLevelMetadata_FailsDeserialization) { |
| const struct { |
| const char* desc; |
| absl::variant<AttributionEventLevelMetadataRecord, std::string> record; |
| bool valid; |
| } kTestCases[] = { |
| { |
| .desc = "invalid_proto", |
| .record = "!", |
| .valid = false, |
| }, |
| { |
| .desc = "missing_priority", |
| .record = |
| AttributionEventLevelMetadataRecord{ |
| .trigger_data = 1, |
| }, |
| .valid = false, |
| }, |
| { |
| .desc = "missing_trigger_data", |
| .record = |
| AttributionEventLevelMetadataRecord{ |
| .priority = 2, |
| }, |
| .valid = false, |
| }, |
| { |
| .desc = "valid", |
| .record = |
| AttributionEventLevelMetadataRecord{ |
| .trigger_data = 1, |
| .priority = 2, |
| }, |
| .valid = true, |
| }, |
| }; |
| |
| for (auto test_case : kTestCases) { |
| OpenDatabase(); |
| storage()->StoreSource(SourceBuilder().Build()); |
| auto sources = storage()->GetActiveSources(); |
| ASSERT_THAT(sources, SizeIs(1)); |
| CloseDatabase(); |
| |
| std::string metadata = |
| absl::visit(base::Overloaded{ |
| [](const AttributionEventLevelMetadataRecord& record) { |
| return SerializeReportMetadata(record); |
| }, |
| [](const std::string& str) { return str; }, |
| }, |
| test_case.record); |
| |
| StoreAttributionReport(AttributionReportRecord{ |
| .report_id = 1, |
| .source_id = *sources.front().source_id(), |
| .external_report_id = DefaultExternalReportID().AsLowercaseString(), |
| .report_type = static_cast<int>(AttributionReport::Type::kEventLevel), |
| .metadata = metadata, |
| }); |
| |
| OpenDatabase(); |
| EXPECT_THAT( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max()), |
| SizeIs(test_case.valid)) |
| << test_case.desc; |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| CloseDatabase(); |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| InvalidAggregatableMetadata_FailsDeserialization) { |
| const struct { |
| const char* desc; |
| absl::variant<AttributionAggregatableMetadataRecord, std::string> record; |
| absl::optional<int64_t> max_budget; |
| bool valid; |
| } kTestCases[] = { |
| { |
| .desc = "invalid_proto", |
| .record = "!", |
| .valid = false, |
| }, |
| { |
| .desc = "missing_contribution", |
| .record = AttributionAggregatableMetadataRecord(), |
| .valid = false, |
| }, |
| { |
| .desc = "missing_contribution_value", |
| .record = |
| AttributionAggregatableMetadataRecord{ |
| .contributions = |
| { |
| AttributionAggregatableMetadataRecord::Contribution{ |
| .high_bits = 1, |
| .low_bits = 2, |
| }, |
| }, |
| }, |
| .valid = false, |
| }, |
| { |
| .desc = "missing_contribution_key", |
| .record = |
| AttributionAggregatableMetadataRecord{ |
| .contributions = |
| { |
| AttributionAggregatableMetadataRecord::Contribution{ |
| .value = 3, |
| }, |
| }, |
| }, |
| .valid = false, |
| }, |
| { |
| .desc = "valid", |
| .record = |
| AttributionAggregatableMetadataRecord{ |
| .contributions = |
| { |
| AttributionAggregatableMetadataRecord::Contribution{ |
| .high_bits = 1, |
| .low_bits = 2, |
| .value = 3, |
| }, |
| }, |
| }, |
| .valid = true, |
| }, |
| { |
| .desc = "invalid_contribution_value", |
| .record = |
| AttributionAggregatableMetadataRecord{ |
| .contributions = |
| { |
| AttributionAggregatableMetadataRecord::Contribution{ |
| .high_bits = 1, |
| .low_bits = 2, |
| .value = 0, |
| }, |
| }, |
| }, |
| .valid = false, |
| }, |
| { |
| .desc = "contribution_value_too_large", |
| .record = |
| AttributionAggregatableMetadataRecord{ |
| .contributions = |
| { |
| AttributionAggregatableMetadataRecord::Contribution{ |
| .high_bits = 1, |
| .low_bits = 2, |
| .value = 11, |
| }, |
| }, |
| }, |
| .max_budget = 10, |
| .valid = false, |
| }, |
| { |
| .desc = "missing_coordinator", |
| .record = |
| AttributionAggregatableMetadataRecord{ |
| .coordinator = absl::nullopt, |
| .contributions = |
| { |
| AttributionAggregatableMetadataRecord::Contribution{ |
| .high_bits = 1, |
| .low_bits = 2, |
| .value = 3, |
| }, |
| }, |
| }, |
| .valid = false, |
| }, |
| { |
| .desc = "missing_source_registration_time_config", |
| .record = |
| AttributionAggregatableMetadataRecord{ |
| .contributions = |
| { |
| AttributionAggregatableMetadataRecord::Contribution{ |
| .high_bits = 1, |
| .low_bits = 2, |
| .value = 3, |
| }, |
| }, |
| .source_registration_time_config = absl::nullopt, |
| }, |
| .valid = false, |
| }, |
| }; |
| |
| for (auto test_case : kTestCases) { |
| OpenDatabase(); |
| storage()->StoreSource(SourceBuilder().Build()); |
| auto sources = storage()->GetActiveSources(); |
| ASSERT_THAT(sources, SizeIs(1)); |
| CloseDatabase(); |
| |
| std::string metadata = absl::visit( |
| base::Overloaded{ |
| [](const AttributionAggregatableMetadataRecord& record) { |
| return SerializeReportMetadata(record); |
| }, |
| [](const std::string& str) { return str; }, |
| }, |
| test_case.record); |
| |
| StoreAttributionReport(AttributionReportRecord{ |
| .report_id = 1, |
| .source_id = *sources.front().source_id(), |
| .external_report_id = DefaultExternalReportID().AsLowercaseString(), |
| .report_type = |
| static_cast<int>(AttributionReport::Type::kAggregatableAttribution), |
| .metadata = metadata, |
| }); |
| |
| OpenDatabase(); |
| if (test_case.max_budget) { |
| delegate()->set_aggregatable_budget_per_source(*test_case.max_budget); |
| } |
| EXPECT_THAT( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max()), |
| SizeIs(test_case.valid)) |
| << test_case.desc; |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| CloseDatabase(); |
| } |
| } |
| |
| TEST_F(AttributionStorageSqlTest, |
| InvalidNullAggregatableMetadata_FailsDeserialization) { |
| const struct { |
| const char* desc; |
| absl::variant<AttributionNullAggregatableMetadataRecord, std::string> |
| record; |
| bool valid; |
| } kTestCases[] = { |
| { |
| .desc = "invalid_proto", |
| .record = "!", |
| .valid = false, |
| }, |
| { |
| .desc = "missing_fake_source_time", |
| .record = AttributionNullAggregatableMetadataRecord(), |
| .valid = false, |
| }, |
| { |
| .desc = "missing_coordinator", |
| .record = |
| AttributionNullAggregatableMetadataRecord{ |
| .coordinator = absl::nullopt, |
| .fake_source_time = 12345678900, |
| }, |
| .valid = false, |
| }, |
| { |
| .desc = "missing_source_registration_time_config", |
| .record = |
| AttributionNullAggregatableMetadataRecord{ |
| .fake_source_time = 12345678900, |
| .source_registration_time_config = absl::nullopt, |
| }, |
| .valid = false, |
| }, |
| { |
| .desc = "valid", |
| .record = |
| AttributionNullAggregatableMetadataRecord{ |
| .fake_source_time = 12345678900, |
| }, |
| .valid = true, |
| }, |
| }; |
| |
| for (auto test_case : kTestCases) { |
| OpenDatabase(); |
| // Create the tables. |
| storage()->StoreSource(SourceBuilder().Build()); |
| auto sources = storage()->GetActiveSources(); |
| ASSERT_THAT(sources, SizeIs(1)); |
| CloseDatabase(); |
| |
| std::string metadata = absl::visit( |
| base::Overloaded{ |
| [](const AttributionNullAggregatableMetadataRecord& record) { |
| return SerializeReportMetadata(record); |
| }, |
| [](const std::string& str) { return str; }, |
| }, |
| test_case.record); |
| |
| StoreAttributionReport(AttributionReportRecord{ |
| .report_id = 1, |
| .source_id = -1, |
| .external_report_id = DefaultExternalReportID().AsLowercaseString(), |
| .report_type = |
| static_cast<int>(AttributionReport::Type::kNullAggregatable), |
| .metadata = metadata, |
| }); |
| |
| OpenDatabase(); |
| EXPECT_THAT( |
| storage()->GetAttributionReports(/*max_report_time=*/base::Time::Max()), |
| SizeIs(test_case.valid)) |
| << test_case.desc; |
| storage()->ClearData(base::Time::Min(), base::Time::Max(), |
| base::NullCallback()); |
| CloseDatabase(); |
| } |
| } |
| |
| } // namespace |
| } // namespace content |