| // Copyright 2021 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/interest_group/interest_group_storage.h" |
| |
| #include <stddef.h> |
| |
| #include <functional> |
| #include <memory> |
| #include <optional> |
| |
| #include "base/base64.h" |
| #include "base/command_line.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/bind.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_clock.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "content/browser/interest_group/bidding_and_auction_server_key_fetcher.h" |
| #include "content/browser/interest_group/for_debugging_only_report_util.h" |
| #include "content/browser/interest_group/interest_group_features.h" |
| #include "content/browser/interest_group/interest_group_storage.pb.h" |
| #include "content/browser/interest_group/interest_group_update.h" |
| #include "content/browser/interest_group/storage_interest_group.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/network_service_instance.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "content/public/test/test_utils.h" |
| #include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h" |
| #include "crypto/sha2.h" |
| #include "services/network/network_service.h" |
| #include "services/network/public/cpp/ad_auction/event_record.h" |
| #include "services/network/public/cpp/features.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 "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/interest_group/ad_auction_constants.h" |
| #include "third_party/blink/public/common/interest_group/interest_group.h" |
| #include "third_party/blink/public/common/interest_group/test/interest_group_test_utils.h" |
| #include "third_party/blink/public/common/interest_group/test_interest_group_builder.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include "base/mac/mac_util.h" |
| #endif |
| |
| namespace content { |
| namespace { |
| |
| using ::blink::IgExpectEqualsForTesting; |
| using ::blink::IgExpectNotEqualsForTesting; |
| using ::blink::InterestGroup; |
| using ::network::AdAuctionEventRecord; |
| using ::testing::Field; |
| using ::testing::Property; |
| using ::testing::UnorderedElementsAre; |
| using ::testing::UnorderedElementsAreArray; |
| using SellerCapabilities = blink::SellerCapabilities; |
| using SellerCapabilitiesType = blink::SellerCapabilitiesType; |
| |
| constexpr char kFullOriginStr[] = "https://full.example.com"; |
| constexpr char kPartialOriginStr[] = "https://partial.example.com"; |
| |
| constexpr char kViewClickEligibleOrigin1Str[] = |
| "https://view-click.eligible1.test"; |
| constexpr char kViewClickEligibleOrigin2Str[] = |
| "https://view-click.eligible2.test"; |
| constexpr char kViewClickProviderOrigin1Str[] = |
| "https://view-click.provider1.test"; |
| constexpr char kViewClickProviderOrigin2Str[] = |
| "https://view-click.provider2.test"; |
| |
| constexpr int kOldestAllFieldsVersion = 28; |
| |
| struct ClickinessCompactionEvents { |
| ListOfTimestamps uncompacted_events; |
| ListOfTimestampAndCounts compacted_events; |
| }; |
| |
| // A convenience wrapper to |
| // InterestGroupStorage::ComputeCompactClickinessForTesting that operates on |
| // protos rather than their serializations. |
| std::optional<ClickinessCompactionEvents> ComputeCompactClickiness( |
| base::Time now, |
| const ClickinessCompactionEvents& events) { |
| InterestGroupStorage::ClickinessCompactionRawEvents raw; |
| EXPECT_TRUE( |
| events.uncompacted_events.SerializeToString(&raw.uncompacted_events)); |
| EXPECT_TRUE(events.compacted_events.SerializeToString(&raw.compacted_events)); |
| std::optional<InterestGroupStorage::ClickinessCompactionRawEvents> |
| compact_raw = |
| InterestGroupStorage::ComputeCompactClickinessForTesting(now, raw); |
| if (!compact_raw) { |
| return std::nullopt; |
| } |
| std::optional<ClickinessCompactionEvents> result; |
| result.emplace(); |
| EXPECT_TRUE(result->uncompacted_events.ParseFromString( |
| compact_raw->uncompacted_events)); |
| EXPECT_TRUE( |
| result->compacted_events.ParseFromString(compact_raw->compacted_events)); |
| return result; |
| } |
| |
| class InterestGroupStorageTest : public testing::Test { |
| public: |
| InterestGroupStorageTest() = default; |
| |
| void SetUp() override { |
| ASSERT_TRUE(temp_directory_.CreateUniqueTempDir()); |
| scoped_feature_list_.InitWithFeaturesAndParameters( |
| {{ |
| network::features::kInterestGroupStorage, |
| {{"max_owners", "10"}, |
| {"max_groups_per_owner", "10"}, |
| {"max_negative_groups_per_owner", "30"}, |
| {"max_ops_before_maintenance", "100"}, |
| {"max_storage_per_owner", "4096"}}, |
| }, |
| {blink::features::kFledgeAuctionDealSupport, {}}}, |
| {}); |
| } |
| |
| // Returns a summary of all interest groups. Each interest group is returned |
| // as a string of the form: |
| // "<origin>;<name>". This allows for easily checking that only the expected |
| // interest groups remain. |
| std::vector<std::string> GetInterestGroupSummary( |
| InterestGroupStorage& storage) { |
| std::vector<content::StorageInterestGroup> groups = |
| storage.GetAllInterestGroupsUnfilteredForTesting(); |
| std::vector<std::string> summary; |
| for (const auto& group : groups) { |
| summary.emplace_back(base::StringPrintf( |
| "%s;%s", group.interest_group.owner.Serialize().c_str(), |
| group.interest_group.name.c_str())); |
| } |
| return summary; |
| } |
| |
| std::unique_ptr<InterestGroupStorage> CreateStorage() { |
| return std::make_unique<InterestGroupStorage>(temp_directory_.GetPath()); |
| } |
| |
| base::FilePath db_path() { |
| return temp_directory_.GetPath().Append( |
| FILE_PATH_LITERAL("InterestGroups")); |
| } |
| |
| content::BrowserTaskEnvironment& task_environment() { |
| return task_environment_; |
| } |
| |
| InterestGroup NewInterestGroup(url::Origin owner, std::string name) { |
| InterestGroup result; |
| result.owner = owner; |
| result.name = name; |
| result.bidding_url = owner.GetURL().Resolve("/bidding_script.js"); |
| result.update_url = owner.GetURL().Resolve("/update_script.js"); |
| result.expiry = base::Time::Now() + blink::MaxInterestGroupLifetime(); |
| result.execution_mode = |
| blink::InterestGroup::ExecutionMode::kCompatibilityMode; |
| return result; |
| } |
| |
| InterestGroup NewNegativeInterestGroup(url::Origin owner, std::string name) { |
| constexpr blink::InterestGroup::AdditionalBidKey kAdditionalBidKey = { |
| 0x7d, 0x4d, 0x0e, 0x7f, 0x61, 0x53, 0xa6, 0x9b, 0x62, 0x42, 0xb5, |
| 0x22, 0xab, 0xbe, 0xe6, 0x85, 0xfd, 0xa4, 0x42, 0x0f, 0x88, 0x34, |
| 0xb1, 0x08, 0xc3, 0xbd, 0xae, 0x36, 0x9e, 0xf5, 0x49, 0xfa}; |
| |
| InterestGroup result; |
| result.owner = owner; |
| result.name = name; |
| result.additional_bid_key = kAdditionalBidKey; |
| result.expiry = base::Time::Now() + blink::MaxInterestGroupLifetime(); |
| return result; |
| } |
| |
| // Produces full interest group. The `version_number` parameter controls the |
| // set of fields added to the full interest group -- only fields that existed |
| // in that version will be added. By default, all fields for the current |
| // version are added. |
| // |
| // For best coverage, changes within the fields that don't require any |
| // active database migration (e.g. adding an optional field to a proto) should |
| // still get their own version number with `version_changed_ig_fields` |
| // set to true. |
| // |
| // If non-null, *version_changed_ig_fields will be set indicating if |
| // `version_number` changed any new interest group fields when compared to |
| // `version_number` - 1, if it exists. (Some versions merely changed the |
| // format of interest groups on disk and didn't add fields, or added |
| // fields that don't affect blink::InterestGroup). This is needed for a check |
| // to ensure that IgExpect[Not]EqualsForTesting() gets updated when adding a |
| // new version. |
| blink::InterestGroup ProduceAllFields( |
| int version_number = -1, |
| bool* version_changed_ig_fields = nullptr) { |
| if (version_number == -1) { |
| version_number = |
| InterestGroupStorage::GetCurrentVersionNumberForTesting(); |
| } |
| |
| if (version_changed_ig_fields) { |
| switch (version_number) { |
| case 9: |
| case 16: |
| case 17: |
| case 18: |
| case 20: |
| case 22: |
| case 24: |
| case 25: |
| case 26: |
| case 27: |
| case 30: |
| case 32: |
| case 34: |
| case 35: |
| *version_changed_ig_fields = false; |
| break; |
| default: |
| *version_changed_ig_fields = true; |
| } |
| } |
| |
| // These fields have been supported for many old versions -- setting them |
| // here avoids having to check and maybe initialize multiple times in each |
| // version. |
| blink::InterestGroup result; |
| result.ads.emplace(); |
| result.ad_components.emplace(); |
| result.ads->emplace_back( |
| /*render_gurl=*/GURL("https://full.example.com/ad1"), |
| /*metadata=*/"metadata1"); |
| result.ads->emplace_back( |
| /*render_gurl=*/GURL("https://full.example.com/ad2"), |
| /*metadata=*/"metadata2"); |
| result.ad_components->emplace_back( |
| /*render_gurl=*/GURL("https://full.example.com/adcomponent1"), |
| /*metadata=*/"metadata1c"); |
| result.ad_components->emplace_back( |
| /*render_gurl=*/GURL("https://full.example.com/adcomponent2"), |
| /*metadata=*/"metadata2c"); |
| result.ad_sizes.emplace(); |
| |
| // ***NOTE***: Please use non-default values in the assignments below -- it |
| // helps validate that non-default fields get upgraded correctly, for |
| // instance. |
| |
| switch (version_number) { |
| case 35: |
| // Added `cached_k_anonymity_hashes` table, but introduced no new |
| // `interest_group` table fields. |
| [[fallthrough]]; |
| case 34: |
| // Added `view_and_click_events` table, but introduced no new |
| // `interest_group` table fields. |
| [[fallthrough]]; |
| case 33: |
| result.view_and_click_counts_providers = {{url::Origin::Create( |
| GURL("https://view-and-click-counts-provider.test"))}}; |
| [[fallthrough]]; |
| case 32: |
| [[fallthrough]]; |
| case 31: |
| result.ads.value()[0].creative_scanning_metadata = "scan 1"; |
| result.ad_components.value()[1].creative_scanning_metadata = "scan 2"; |
| [[fallthrough]]; |
| case 30: |
| // Compressed AdsProto, but introduced no new fields. |
| [[fallthrough]]; |
| case 29: |
| result.ads.value()[0].selectable_buyer_and_seller_reporting_ids = { |
| "selectable_id1", "selectable_id2"}; |
| [[fallthrough]]; |
| case 28: |
| // NOTE: As this is the oldest version supported by ProduceAllFields(), |
| // it also initializes fields from before version 28. |
| EXPECT_EQ(kOldestAllFieldsVersion, 28); |
| |
| result.owner = kFullOrigin; |
| result.name = "full"; |
| result.priority = 1.0; |
| result.enable_bidding_signals_prioritization = true; |
| result.priority_vector = {{{"a", 2}, {"b", -2.2}}}; |
| result.priority_signals_overrides = { |
| {{"a", -2}, {"c", 10}, {"d", 1.2}}}; |
| result.seller_capabilities = { |
| {{kFullOrigin, {SellerCapabilities::kInterestGroupCounts}}, |
| {kPartialOrigin, {SellerCapabilities::kLatencyStats}}}}; |
| result.all_sellers_capabilities = { |
| SellerCapabilities::kInterestGroupCounts, |
| SellerCapabilities::kLatencyStats}; |
| result.execution_mode = |
| blink::InterestGroup::ExecutionMode::kFrozenContext; |
| result.bidding_url = GURL("https://full.example.com/bid"); |
| result.bidding_wasm_helper_url = |
| GURL("https://full.example.com/bid_wasm"); |
| result.update_url = GURL("https://full.example.com/update"); |
| result.trusted_bidding_signals_url = |
| GURL("https://full.example.com/signals"); |
| result.trusted_bidding_signals_keys = {"a", "b", "c", "d"}; |
| result.trusted_bidding_signals_slot_size_mode = blink::InterestGroup:: |
| TrustedBiddingSignalsSlotSizeMode::kAllSlotsRequestedSizes; |
| result.max_trusted_bidding_signals_url_length = 8000; |
| result.trusted_bidding_signals_coordinator = |
| url::Origin::Create(GURL("https://coordinator.test/")); |
| result.user_bidding_signals = "foo"; |
| result.ads.value()[0].size_group = "group_1"; |
| result.ads.value()[0].buyer_reporting_id = "buyer_id"; |
| result.ads.value()[0].buyer_and_seller_reporting_id = "shared_id"; |
| result.ads.value()[0].ad_render_id = "adRenderId"; |
| result.ads.value()[0].allowed_reporting_origins = { |
| {url::Origin::Create(GURL("https://reporting.com"))}}; |
| result.ads.value()[1].size_group = "group_2"; |
| result.ads.value()[1].buyer_reporting_id = "buyer_id2"; |
| result.ad_components.value()[0].size_group = "group_1"; |
| result.ad_components.value()[0].ad_render_id = "adRenderId2"; |
| result.ad_components.value()[1].size_group = "group_2"; |
| result.ad_sizes.value()["size_1"].width = 300; |
| result.ad_sizes.value()["size_1"].width_units = |
| blink::AdSize::LengthUnit::kPixels; |
| result.ad_sizes.value()["size_1"].height = 150; |
| result.ad_sizes.value()["size_1"].height_units = |
| blink::AdSize::LengthUnit::kPixels; |
| result.ad_sizes.value()["size_2"].width = 640; |
| result.ad_sizes.value()["size_2"].width_units = |
| blink::AdSize::LengthUnit::kPixels; |
| result.ad_sizes.value()["size_2"].height = 480; |
| result.ad_sizes.value()["size_2"].height_units = |
| blink::AdSize::LengthUnit::kPixels; |
| result.ad_sizes.value()["size_3"].width = 100; |
| result.ad_sizes.value()["size_3"].width_units = |
| blink::AdSize::LengthUnit::kScreenWidth; |
| result.ad_sizes.value()["size_3"].height = 100; |
| result.ad_sizes.value()["size_3"].height_units = |
| blink::AdSize::LengthUnit::kScreenWidth; |
| result.size_groups = { |
| {{"group_1", std::vector<std::string>{"size_1"}}, |
| {"group_2", std::vector<std::string>{"size_1", "size_2"}}, |
| {"group_3", std::vector<std::string>{"size_3"}}}}; |
| result.auction_server_request_flags = { |
| blink::AuctionServerRequestFlagsEnum::kOmitAds, |
| blink::AuctionServerRequestFlagsEnum::kIncludeFullAds}; |
| // Note that `additional_bid_key` can only be set for negative |
| // interest groups, so cannot be set here. |
| result.aggregation_coordinator_origin = |
| url::Origin::Create(GURL("https://coordinator.test/")); |
| break; |
| default: |
| ADD_FAILURE() |
| << "Requested version number " << version_number |
| << " isn't currently supported by ProduceAllFields(). Please " |
| "update ProduceAllFields() for this version -- and if " |
| "appropriate, mark this version's `version_changed_ig_fields` " |
| "as false."; |
| } |
| |
| // Set to a valid non-expired time, to match InterestGroupBuilder. Note that |
| // Now() will change each run (time starts at the actual current time, even |
| // with MOCK_TIME), so upgrade tests will need to ignore the expiry. |
| result.expiry = base::Time::Now() + blink::MaxInterestGroupLifetime(); |
| |
| return result; |
| } |
| |
| // This test is in a helper function so that it can also be run after |
| // UpgradeFromV6. |
| void StoresAllFieldsTest() { |
| InterestGroup partial = NewInterestGroup(kPartialOrigin, "partial"); |
| |
| InterestGroup full = ProduceAllFields(); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->JoinInterestGroup(partial, kPartialOrigin.GetURL()); |
| storage->JoinInterestGroup(full, kFullOrigin.GetURL()); |
| |
| std::vector<StorageInterestGroup> storage_interest_groups = |
| storage->GetInterestGroupsForOwner(kPartialOrigin); |
| ASSERT_EQ(1u, storage_interest_groups.size()); |
| IgExpectEqualsForTesting( |
| /*actual=*/storage_interest_groups[0].interest_group, |
| /*expected=*/partial); |
| |
| storage_interest_groups = storage->GetInterestGroupsForOwner(kFullOrigin); |
| ASSERT_EQ(1u, storage_interest_groups.size()); |
| IgExpectEqualsForTesting( |
| /*actual=*/storage_interest_groups[0].interest_group, |
| /*expected=*/full); |
| base::Time join_time = base::Time::Now(); |
| EXPECT_EQ(storage_interest_groups[0].join_time, join_time); |
| EXPECT_EQ(storage_interest_groups[0].last_updated, join_time); |
| |
| // Test update as well. |
| |
| // Pass time, so can check if `join_time` or `last_updated` is updated. |
| task_environment().FastForwardBy(base::Seconds(1234)); |
| |
| InterestGroupUpdate update; |
| update.bidding_url = GURL("https://full.example.com/bid2"); |
| update.bidding_wasm_helper_url = GURL("https://full.example.com/bid_wasm2"); |
| update.trusted_bidding_signals_url = |
| GURL("https://full.example.com/signals2"); |
| update.trusted_bidding_signals_keys = |
| std::vector<std::string>{"a", "b2", "c", "d"}; |
| update.ads = full.ads; |
| update.ads->emplace_back( |
| GURL("https://full.example.com/ad3"), "metadata3", "group_3", |
| "new_buyer_id", "another_share_id", |
| std::vector<std::string>{"new_selectable_id1", "new_selectable_id2"}, |
| "adRenderId3", |
| std::vector<url::Origin>{ |
| url::Origin::Create(GURL("https://reporting.updated.com"))}); |
| update.ad_components = full.ad_components; |
| update.ad_components->emplace_back( |
| GURL("https://full.example.com/adcomponent3"), "metadata3c", "group_3"); |
| storage->UpdateInterestGroup(blink::InterestGroupKey(full.owner, full.name), |
| update); |
| |
| InterestGroup updated = full; |
| updated.bidding_url = update.bidding_url; |
| updated.bidding_wasm_helper_url = update.bidding_wasm_helper_url; |
| updated.trusted_bidding_signals_url = update.trusted_bidding_signals_url; |
| updated.trusted_bidding_signals_keys = update.trusted_bidding_signals_keys; |
| updated.ads = update.ads; |
| updated.ad_components = update.ad_components; |
| |
| storage_interest_groups = storage->GetInterestGroupsForOwner(kFullOrigin); |
| ASSERT_EQ(1u, storage_interest_groups.size()); |
| IgExpectEqualsForTesting( |
| /*actual=*/storage_interest_groups[0].interest_group, |
| /*expected=*/updated); |
| // `join_time` should not be modified be updates, but `last_updated` should |
| // be. |
| EXPECT_EQ(storage_interest_groups[0].join_time, join_time); |
| EXPECT_EQ(storage_interest_groups[0].last_updated, base::Time::Now()); |
| // Make sure the clock was advanced. |
| EXPECT_NE(storage_interest_groups[0].join_time, |
| storage_interest_groups[0].last_updated); |
| } |
| |
| const url::Origin kFullOrigin = url::Origin::Create(GURL(kFullOriginStr)); |
| const url::Origin kPartialOrigin = |
| url::Origin::Create(GURL(kPartialOriginStr)); |
| |
| const url::Origin kViewClickEligibleOrigin1 = |
| url::Origin::Create(GURL(kViewClickEligibleOrigin1Str)); |
| const url::Origin kViewClickEligibleOrigin2 = |
| url::Origin::Create(GURL(kViewClickEligibleOrigin2Str)); |
| const url::Origin kViewClickProviderOrigin1 = |
| url::Origin::Create(GURL(kViewClickProviderOrigin1Str)); |
| const url::Origin kViewClickProviderOrigin2 = |
| url::Origin::Create(GURL(kViewClickProviderOrigin2Str)); |
| |
| base::ScopedTempDir temp_directory_; |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| content::BrowserTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| }; |
| |
| TEST_F(InterestGroupStorageTest, DatabaseInitialized_CreateDatabase) { |
| EXPECT_FALSE(base::PathExists(db_path())); |
| |
| { std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); } |
| |
| // InterestGroupStorageSqlImpl opens the database lazily. |
| EXPECT_FALSE(base::PathExists(db_path())); |
| |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| storage->LeaveInterestGroup(blink::InterestGroupKey(test_origin, "example"), |
| test_origin); |
| } |
| |
| // InterestGroupStorage creates the database if it doesn't exist. |
| EXPECT_TRUE(base::PathExists(db_path())); |
| |
| { |
| sql::Database raw_db(sql::test::kTestTag); |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| |
| // [interest_groups], [join_history], [bid_history], [win_history], |
| // [joined_k_anon], [meta], [lockout_debugging_only_report], |
| // [cooldown_debugging_only_report], [bidding_and_auction_server_keys], |
| // [view_and_click_events], [cached_k_anonymity_hashes]. |
| EXPECT_EQ(11u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema(); |
| } |
| } |
| |
| TEST_F(InterestGroupStorageTest, DatabaseRazesOldVersion) { |
| ASSERT_FALSE(base::PathExists(db_path())); |
| |
| // Create an empty database with old schema version (version=1). |
| { |
| sql::Database raw_db(sql::test::kTestTag); |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| |
| sql::MetaTable meta_table; |
| EXPECT_TRUE( |
| meta_table.Init(&raw_db, /*version=*/1, /*compatible_version=*/1)); |
| |
| EXPECT_EQ(1u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema(); |
| } |
| |
| EXPECT_TRUE(base::PathExists(db_path())); |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| // We need to perform an interest group operation to trigger DB |
| // initialization. |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| storage->LeaveInterestGroup(blink::InterestGroupKey(test_origin, "example"), |
| test_origin); |
| } |
| |
| { |
| sql::Database raw_db(sql::test::kTestTag); |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| |
| // [interest_groups], [join_history], [bid_history], [win_history], |
| // [joined_k_anon], [meta], [lockout_debugging_only_report], |
| // [cooldown_debugging_only_report], [bidding_and_auction_server_keys], |
| // [view_and_click_events], [cached_k_anonymity_hashes]. |
| EXPECT_EQ(11u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema(); |
| } |
| } |
| |
| TEST_F(InterestGroupStorageTest, DatabaseRazesNewVersion) { |
| ASSERT_FALSE(base::PathExists(db_path())); |
| |
| // Create an empty database with a newer schema version (version=1000000). |
| { |
| sql::Database raw_db(sql::test::kTestTag); |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| |
| sql::MetaTable meta_table; |
| EXPECT_TRUE(meta_table.Init(&raw_db, /*version=*/1000000, |
| /*compatible_version=*/1000000)); |
| |
| EXPECT_EQ(1u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema(); |
| } |
| |
| EXPECT_TRUE(base::PathExists(db_path())); |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| // We need to perform an interest group operation to trigger DB |
| // initialization. |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| storage->LeaveInterestGroup(blink::InterestGroupKey(test_origin, "example"), |
| test_origin); |
| } |
| |
| { |
| sql::Database raw_db(sql::test::kTestTag); |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| |
| // [interest_groups], [join_history], [bid_history], [win_history], |
| // [joined_k_anon], [meta], [lockout_debugging_only_report], |
| // [cooldown_debugging_only_report], [bidding_and_auction_server_keys], |
| // [view_and_click_events], [cached_k_anonymity_hashes]. |
| EXPECT_EQ(11u, sql::test::CountSQLTables(&raw_db)) << raw_db.GetSchema(); |
| } |
| } |
| |
| TEST_F(InterestGroupStorageTest, DatabaseJoin) { |
| base::HistogramTester histograms; |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| InterestGroup test_group = NewInterestGroup(test_origin, "example"); |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| storage->JoinInterestGroup(test_group, test_origin.GetURL()); |
| } |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| EXPECT_EQ(test_origin, origins[0]); |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ(test_origin, interest_groups[0].interest_group.owner); |
| EXPECT_EQ("example", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| EXPECT_EQ(interest_groups[0].joining_origin, test_origin); |
| EXPECT_EQ(interest_groups[0].join_time, base::Time::Now()); |
| EXPECT_EQ(interest_groups[0].last_updated, base::Time::Now()); |
| EXPECT_EQ(interest_groups[0].next_update_after, base::Time::Min()); |
| } |
| histograms.ExpectUniqueSample("Storage.InterestGroup.PerSiteCount", 1u, 1); |
| } |
| |
| TEST_F(InterestGroupStorageTest, GetGroupDoesNotReturnOutdatedKanonKeys) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| GURL ad2_url = GURL("https://owner.example.com/ad2"); |
| |
| InterestGroup g = NewInterestGroup(test_origin, "name"); |
| blink::InterestGroupKey group_key(g.owner, g.name); |
| std::vector<InterestGroup::Ad> ads; |
| std::vector<InterestGroup::Ad> ad_components; |
| ads.emplace_back( |
| ad1_url, "metadata1", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid1", |
| /*buyer_and_seller_reporting_id=*/"shrid1", |
| /*selectable_buyer_and_seller_reporting_ids=*/ |
| std::vector<std::string>{"selectable_id1", "selectable_id2"}); |
| ads.emplace_back(ad2_url, "metadata2", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid2", |
| /*buyer_and_seller_reporting_id=*/std::nullopt); |
| ads.emplace_back(ad2_url, "metadata3", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/std::nullopt, |
| /*buyer_and_seller_reporting_id=*/std::nullopt); |
| ad_components.emplace_back(ad2_url, "component_metadata3"); |
| ad_components.emplace_back(ad1_url, "component_metadata1"); |
| |
| g.ads = ads; |
| g.ad_components = ad_components; |
| std::string kanon_bid1 = blink::HashedKAnonKeyForAdBid(g, ad1_url.spec()); |
| std::string kanon_report1 = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[0], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| std::string kanon_report1a = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[0], std::string("selectable_id1")); |
| std::string kanon_report1b = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[0], std::string("selectable_id2")); |
| std::string kanon_bid2 = blink::HashedKAnonKeyForAdBid(g, ad2_url.spec()); |
| std::string kanon_report2 = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[1], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| std::string kanon_component_1 = blink::HashedKAnonKeyForAdComponentBid( |
| g.ad_components.value()[0].render_url()); |
| |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| std::vector<std::string> expected_positive_returned_keys = { |
| kanon_bid1, kanon_report1, kanon_report1a, kanon_report1b, |
| kanon_bid2, kanon_report2, kanon_component_1}; |
| storage->UpdateKAnonymity(group_key, expected_positive_returned_keys, |
| base::Time::Now(), true); |
| |
| EXPECT_THAT( |
| storage->GetInterestGroup(group_key)->hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_returned_keys)); |
| |
| // Make some keys outdated via another join. |
| g.ad_components.reset(); |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| |
| expected_positive_returned_keys.pop_back(); // Remove the component key. |
| EXPECT_THAT( |
| storage->GetInterestGroup(group_key)->hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_returned_keys)); |
| |
| // Make some keys outdated via an update. |
| InterestGroupUpdate update; |
| update.ads = {ads[0]}; |
| storage->UpdateInterestGroup(group_key, update); |
| |
| expected_positive_returned_keys = {kanon_bid1, kanon_report1, kanon_report1a, |
| kanon_report1b}; |
| EXPECT_THAT( |
| storage->GetInterestGroup(group_key)->hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_returned_keys)); |
| } |
| |
| TEST_F(InterestGroupStorageTest, |
| JoinAndUpdateReturnCorrectKanonUpdateParameter) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| GURL ad2_url = GURL("https://owner.example.com/ad2"); |
| GURL ad3_url = GURL("https://owner.example.com/ad3"); |
| |
| InterestGroup g = NewInterestGroup(test_origin, "name"); |
| blink::InterestGroupKey group_key(g.owner, g.name); |
| std::vector<InterestGroup::Ad> ads; |
| std::vector<InterestGroup::Ad> ad_components; |
| ads.emplace_back( |
| ad1_url, "metadata1", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid1", |
| /*buyer_and_seller_reporting_id=*/"shrid1", |
| /*selectable_buyer_and_seller_reporting_ids=*/ |
| std::vector<std::string>{"selectable_id1", "selectable_id2"}); |
| ads.emplace_back(ad2_url, "metadata2", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid2", |
| /*buyer_and_seller_reporting_id=*/std::nullopt); |
| ads.emplace_back(ad3_url, "metadata2", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid2", |
| /*buyer_and_seller_reporting_id=*/std::nullopt); |
| ad_components.emplace_back(ad3_url, "component_metadata3"); |
| ad_components.emplace_back(ad1_url, "component_metadata1"); |
| |
| g.ads = ads; |
| g.ad_components = ad_components; |
| std::string kanon_bid1 = blink::HashedKAnonKeyForAdBid(g, ad1_url.spec()); |
| std::string kanon_report1 = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[0], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| std::string kanon_report1a = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[0], std::string("selectable_id1")); |
| std::string kanon_report1b = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[0], std::string("selectable_id2")); |
| std::string kanon_bid2 = blink::HashedKAnonKeyForAdBid(g, ad2_url.spec()); |
| std::string kanon_report2 = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[1], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| std::string kanon_bid3 = blink::HashedKAnonKeyForAdBid(g, ad3_url.spec()); |
| std::string kanon_report3 = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[2], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| std::string kanon_component_1 = blink::HashedKAnonKeyForAdComponentBid( |
| g.ad_components.value()[0].render_url()); |
| std::string kanon_component_2 = blink::HashedKAnonKeyForAdComponentBid( |
| g.ad_components.value()[1].render_url()); |
| |
| // Join a new interest group. |
| { |
| g.ads->clear(); |
| g.ad_components->clear(); |
| g.ads->emplace_back(ads[0]); |
| g.ads->emplace_back(ads[1]); |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| // The keys have never been updated. |
| EXPECT_EQ(k_anon_update_data->update_time, base::Time::Min()); |
| // All keys are new. All keys are returned. |
| std::vector<std::string> all_kanon_keys = {kanon_bid1, kanon_bid2, |
| kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_report2}; |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray(all_kanon_keys)); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::UnorderedElementsAreArray(all_kanon_keys)); |
| } |
| |
| // Update the k-anonymity for the interest group so that we can make |
| // sure we get the correct update_time. |
| base::Time update_time = base::Time::Now(); |
| storage->UpdateKAnonymity(group_key, {kanon_bid1}, update_time, true); |
| |
| // Join an existing interest group with the exact same ads. |
| { |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| std::vector<std::string> all_kanon_keys = {kanon_bid1, kanon_bid2, |
| kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_report2}; |
| // No new keys. The set of all keys is the same. |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray(all_kanon_keys)); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::IsEmpty()); |
| } |
| |
| // Join an existing interest group with additional ads. |
| { |
| g.ads->emplace_back(ads[2]); |
| g.ad_components = ad_components; |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| std::vector<std::string> all_kanon_keys = { |
| kanon_bid1, kanon_bid2, kanon_bid3, kanon_report1, |
| kanon_report1a, kanon_report1b, kanon_report2, kanon_report3, |
| kanon_component_1, kanon_component_2}; |
| // Expect that the new keys are represented. |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray(all_kanon_keys)); |
| EXPECT_THAT( |
| k_anon_update_data->newly_added_hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid3, kanon_report3, kanon_component_1, kanon_component_2})); |
| } |
| |
| // Join an interest group containing a subset of the ads in the current group. |
| { |
| g.ads = {ads[0]}; |
| g.ad_components = {ad_components[1]}; |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| std::vector<std::string> all_kanon_keys = {kanon_bid1, kanon_report1, |
| kanon_report1a, kanon_report1b, |
| kanon_component_2}; |
| // There are no new keys. |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray(all_kanon_keys)); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::IsEmpty()); |
| } |
| |
| // Join an interest group with new keys and just one of the existing keys. |
| { |
| g.ads = {ads[0], ads[1], ads[2]}; |
| g.ad_components = {ad_components[0]}; |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| std::vector<std::string> all_kanon_keys = { |
| kanon_bid1, kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_bid2, kanon_report2, |
| kanon_bid3, kanon_report3, kanon_component_1}; |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray(all_kanon_keys)); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::UnorderedElementsAreArray({kanon_bid2, kanon_report2, |
| kanon_bid3, kanon_report3, |
| kanon_component_1})); |
| } |
| |
| // Join an interest group with no ads. |
| { |
| g.ads->clear(); |
| g.ad_components->clear(); |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| // There are no new keys. |
| EXPECT_THAT(k_anon_update_data->hashed_keys, testing::IsEmpty()); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::IsEmpty()); |
| } |
| |
| InterestGroupUpdate update; |
| |
| // Do an interest group update that has new ad components. |
| { |
| update.ad_components = ad_components; |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->UpdateInterestGroup(group_key, update); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_component_1, kanon_component_2})); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_component_1, kanon_component_2})); |
| } |
| |
| // Do an interest group update that has no new ads or ad components. |
| { |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->UpdateInterestGroup(group_key, update); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_component_1, kanon_component_2})); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::IsEmpty()); |
| } |
| |
| // Do an interest group update that has new ads. |
| { |
| update.ads = {ads[0], ads[1]}; |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->UpdateInterestGroup(group_key, update); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid1, kanon_bid2, kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_report2, kanon_component_1, |
| kanon_component_2})); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid1, kanon_bid2, kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_report2})); |
| } |
| |
| // Do an interest group update that doesn't have the ads or ad_components |
| // fields. |
| { |
| update.ads.reset(); |
| update.ad_components.reset(); |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->UpdateInterestGroup(group_key, update); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid1, kanon_bid2, kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_report2, kanon_component_1, |
| kanon_component_2})); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::IsEmpty()); |
| } |
| |
| // Do an interest group update that updates the bidding URL. This will affect |
| // the reporting and bidding k-anon keys. |
| { |
| update.bidding_url = GURL("https://owner.example.com/bid2"); |
| |
| // Recalculate our keys with the new bidding URL. |
| g.ads = {ads[0], ads[1]}; |
| g.bidding_url = update.bidding_url; |
| kanon_bid1 = blink::HashedKAnonKeyForAdBid(g, ad1_url.spec()); |
| kanon_bid2 = blink::HashedKAnonKeyForAdBid(g, ad2_url.spec()); |
| kanon_report1 = blink::HashedKAnonKeyForAdNameReporting( |
| g, ads[0], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| kanon_report1a = blink::HashedKAnonKeyForAdNameReporting( |
| g, ads[0], std::string("selectable_id1")); |
| kanon_report1b = blink::HashedKAnonKeyForAdNameReporting( |
| g, ads[0], std::string("selectable_id2")); |
| kanon_report2 = blink::HashedKAnonKeyForAdNameReporting( |
| g, ads[1], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->UpdateInterestGroup(group_key, update); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid1, kanon_bid2, kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_report2, kanon_component_1, |
| kanon_component_2})); |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid1, kanon_bid2, kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_report2})); |
| } |
| |
| // Limit the number of deals per ad on which k-anon keys are fetched. |
| { |
| base::test::ScopedFeatureList scoped_feature_to_enforce_limit; |
| scoped_feature_to_enforce_limit.InitAndEnableFeatureWithParameters( |
| blink::features:: |
| kFledgeLimitSelectableBuyerAndSellerReportingIdsFetchedFromKAnon, |
| {{"SelectableBuyerAndSellerReportingIdsFetchedFromKAnonLimit", "1"}}); |
| |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->UpdateInterestGroup(group_key, update); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| // `kanon_report1b` is notably absent because of the configured limit. |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid1, kanon_bid2, kanon_report1, kanon_report1a, |
| kanon_report2, kanon_component_1, kanon_component_2})); |
| // Empty because all of these keys were loaded in the previous test. |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::IsEmpty()); |
| } |
| |
| // Similar to the above, but with the Limit the number of deals per ad on |
| // which k-anon keys are fetched set to -1, which enforces no limit. |
| { |
| base::test::ScopedFeatureList scoped_feature_to_enforce_limit; |
| scoped_feature_to_enforce_limit.InitAndEnableFeatureWithParameters( |
| blink::features:: |
| kFledgeLimitSelectableBuyerAndSellerReportingIdsFetchedFromKAnon, |
| {{"SelectableBuyerAndSellerReportingIdsFetchedFromKAnonLimit", "-1"}}); |
| |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->UpdateInterestGroup(group_key, update); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| // `kanon_report1b` is notably back in this list. |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid1, kanon_bid2, kanon_report1, kanon_report1a, |
| kanon_report1b, kanon_report2, kanon_component_1, |
| kanon_component_2})); |
| // Empty because all of these keys were loaded in the previous test. |
| EXPECT_THAT(k_anon_update_data->newly_added_hashed_keys, |
| testing::IsEmpty()); |
| } |
| |
| // Do an interest group update that updates the bidding URL, ads, and ad |
| // components. |
| { |
| update.bidding_url = GURL("https://owner.example.com/bid1"); |
| update.ads = {ads[2]}; |
| update.ad_components = {ad_components[0]}; |
| |
| // Recalculate our keys with the new bidding URL. |
| g.ads = {ads[2]}; |
| g.bidding_url = update.bidding_url; |
| kanon_bid3 = blink::HashedKAnonKeyForAdBid(g, ad3_url.spec()); |
| kanon_report3 = blink::HashedKAnonKeyForAdNameReporting( |
| g, ads[2], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| |
| std::optional<InterestGroupKanonUpdateParameter> k_anon_update_data = |
| storage->UpdateInterestGroup(group_key, update); |
| ASSERT_TRUE(k_anon_update_data.has_value()); |
| EXPECT_EQ(k_anon_update_data->update_time, update_time); |
| EXPECT_THAT(k_anon_update_data->hashed_keys, |
| testing::UnorderedElementsAreArray( |
| {kanon_bid3, kanon_report3, kanon_component_1})); |
| EXPECT_THAT( |
| k_anon_update_data->newly_added_hashed_keys, |
| testing::UnorderedElementsAreArray({kanon_bid3, kanon_report3})); |
| } |
| |
| // Validate the interest groups's 'next_update_after' field. |
| { |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(1u, interest_groups.size()); |
| |
| EXPECT_EQ(interest_groups[0].next_update_after, |
| base::Time::Now() + |
| InterestGroupStorage::kUpdateSucceededBackoffPeriod); |
| } |
| } |
| |
| TEST_F(InterestGroupStorageTest, ValidateInterestGroupOnUpdate) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| |
| InterestGroup g = NewInterestGroup(test_origin, "name"); |
| blink::InterestGroupKey group_key(g.owner, g.name); |
| |
| // To cause IsValid and IsValidForJoinAndUpdate to return false, we use the |
| // hard limit and soft limit, respectively, on the number of |
| // selectableBuyerAndSellerReportingIds. We have two here, so we'll use a |
| // limit of one to cause this ad to make its enclosing interest group invalid. |
| InterestGroup::Ad ad1( |
| ad1_url, "metadata1", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid1", |
| /*buyer_and_seller_reporting_id=*/"shrid1", |
| /*selectable_buyer_and_seller_reporting_ids=*/ |
| std::vector<std::string>{"selectable_id1", "selectable_id2"}); |
| |
| InterestGroup::Ad ad2(ad1_url, "metadata1", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid1", |
| /*buyer_and_seller_reporting_id=*/"shrid1", |
| /*selectable_buyer_and_seller_reporting_ids=*/ |
| std::vector<std::string>{"selectable_id1"}); |
| |
| g.ads = {ad1}; |
| |
| // Join a new interest group. |
| { |
| EXPECT_TRUE( |
| storage->JoinInterestGroup(g, test_origin.GetURL()).has_value()); |
| } |
| |
| InterestGroupUpdate update; |
| { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeatureWithParameters( |
| blink::features::kFledgeLimitSelectableBuyerAndSellerReportingIds, |
| {{"SelectableBuyerAndSellerReportingIdsSoftLimit", "1"}, |
| {"SelectableBuyerAndSellerReportingIdsHardLimit", "1"}}); |
| |
| // The interest group in the database is invalid because it has an ad |
| // whose number of selectableBuyerAndSellerReportingIds exceeds the hard |
| // limit. This update makes it valid. |
| update.ads = {ad2}; |
| EXPECT_TRUE(storage->UpdateInterestGroup(group_key, update).has_value()); |
| |
| // Trying to put back the ad with too many |
| // selectableBuyerAndSellerReportingIds fails. |
| update.ads = {ad1}; |
| EXPECT_FALSE(storage->UpdateInterestGroup(group_key, update).has_value()); |
| } |
| |
| { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeatureWithParameters( |
| blink::features::kFledgeLimitSelectableBuyerAndSellerReportingIds, |
| {{"SelectableBuyerAndSellerReportingIdsSoftLimit", "2"}, |
| {"SelectableBuyerAndSellerReportingIdsHardLimit", "2"}}); |
| // With the limit relaxed, this update passes this time. |
| update.ads = {ad1}; |
| EXPECT_TRUE(storage->UpdateInterestGroup(group_key, update).has_value()); |
| } |
| |
| { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeatureWithParameters( |
| blink::features::kFledgeLimitSelectableBuyerAndSellerReportingIds, |
| {{"SelectableBuyerAndSellerReportingIdsSoftLimit", "1"}, |
| {"SelectableBuyerAndSellerReportingIdsHardLimit", "2"}}); |
| |
| // The interest group in the database is valid because it's still within |
| // the hard limit. This update works because it's still within the soft |
| // limit. |
| update.ads = {ad2}; |
| EXPECT_TRUE(storage->UpdateInterestGroup(group_key, update).has_value()); |
| |
| // But this update back to the ad with too many |
| // selectableBuyerAndSellerReportingIds fails because it exceeds the soft |
| // limit. |
| update.ads = {ad1}; |
| EXPECT_FALSE(storage->UpdateInterestGroup(group_key, update).has_value()); |
| } |
| } |
| |
| // Test that joining an interest group twice increments the counter. |
| // Test that joining multiple interest groups with the same owner only creates a |
| // single distinct owner. Test that leaving one interest group does not affect |
| // membership of other interest groups by the same owner. |
| TEST_F(InterestGroupStorageTest, JoinJoinLeave) { |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->JoinInterestGroup(NewInterestGroup(test_origin, "example"), |
| test_origin.GetURL()); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Advance time so can verify that `join_time` and `last_updated` are updated. |
| task_environment().FastForwardBy(base::Seconds(125)); |
| storage->JoinInterestGroup(NewInterestGroup(test_origin, "example"), |
| test_origin.GetURL()); |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| EXPECT_EQ(test_origin, origins[0]); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("example", interest_groups[0].interest_group.name); |
| EXPECT_EQ(2, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| EXPECT_EQ(interest_groups[0].join_time, base::Time::Now()); |
| EXPECT_EQ(interest_groups[0].last_updated, base::Time::Now()); |
| |
| storage->JoinInterestGroup(NewInterestGroup(test_origin, "example2"), |
| test_origin.GetURL()); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(2u, interest_groups.size()); |
| |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| EXPECT_EQ(test_origin, origins[0]); |
| |
| storage->LeaveInterestGroup(blink::InterestGroupKey(test_origin, "example"), |
| test_origin); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("example2", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| EXPECT_EQ(test_origin, origins[0]); |
| } |
| |
| // Test ClearOriginJoinedInterestGroups(). |
| // |
| // Join the following interest groups: |
| // * With joining origin A, join 3 interest groups with owner B, 2 with an |
| // `executionMode` of "group-by-origin". |
| // * With joining origin A, join 1 interest group with owner C. |
| // * With joining origin site C, join 1 interest group with owner B. |
| // |
| // Then call ClearOriginJoinedInterestGroups() from origin A with owner B |
| // a number of times, making sure that only the expected IGs are deleted |
| // each time. |
| TEST_F(InterestGroupStorageTest, ClearOriginJoinedInterestGroups) { |
| const url::Origin kOriginA = url::Origin::Create(GURL("https://a.test")); |
| const url::Origin kOriginB = url::Origin::Create(GURL("https://b.test")); |
| const url::Origin kOriginC = url::Origin::Create(GURL("https://c.test")); |
| const char kName1[] = "name1"; |
| const char kName2[] = "name2"; |
| const char kName3[] = "name3"; |
| const char kName4[] = "name4"; |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| // Join 3 interest groups owned by kOriginB on kOriginA, with kName1, kName2, |
| // and kName3. The latter two have "group-by-origin" execution mode. |
| storage->JoinInterestGroup(NewInterestGroup(kOriginB, kName1), |
| kOriginA.GetURL()); |
| InterestGroup interest_group = NewInterestGroup(kOriginB, kName2); |
| interest_group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(interest_group, kOriginA.GetURL()); |
| interest_group = NewInterestGroup(kOriginB, kName3); |
| interest_group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(interest_group, kOriginA.GetURL()); |
| |
| // Join an interest group owned by kOriginC from kOriginA. This should not be |
| // left by when calling ClearOriginJoinedInterestGroups() from kOriginA for |
| // kOriginB's interest groups. |
| storage->JoinInterestGroup(NewInterestGroup(kOriginC, kName1), |
| kOriginA.GetURL()); |
| |
| // Join an interest group owned by kOriginB from kOriginC. This should not be |
| // left by when calling ClearOriginJoinedInterestGroups() from kOriginA for |
| // kOriginB's interest groups. |
| storage->JoinInterestGroup(NewInterestGroup(kOriginB, kName4), |
| kOriginC.GetURL()); |
| |
| // Clear all of origin B's interest groups joined from origin B. This should |
| // leave no interest groups. |
| storage->ClearOriginJoinedInterestGroups(kOriginB, {}, |
| /*main_frame_origin=*/kOriginB); |
| EXPECT_THAT(GetInterestGroupSummary(*storage), |
| testing::UnorderedElementsAre( |
| // Origin B's groups that were joined on origin A. |
| "https://b.test;name1", "https://b.test;name2", |
| "https://b.test;name3", |
| // Other groups. |
| "https://c.test;name1", "https://b.test;name4")); |
| |
| // Leave all of origin's B's interest groups joined from origin A, except for |
| // a list that contains all of the groups actually joined that way (plus an |
| // extra group). No groups should be left. |
| EXPECT_THAT(storage->ClearOriginJoinedInterestGroups( |
| kOriginB, {kName1, kName2, kName3, "not-present-group"}, |
| /*main_frame_origin=*/kOriginA), |
| testing::UnorderedElementsAre()); |
| EXPECT_THAT(GetInterestGroupSummary(*storage), |
| testing::UnorderedElementsAre( |
| // Origin B's groups that were joined on origin A. |
| "https://b.test;name1", "https://b.test;name2", |
| "https://b.test;name3", |
| // Other groups. |
| "https://c.test;name1", "https://b.test;name4")); |
| |
| // Leave all of origin's B's interest groups joined from origin A, except for |
| // kName1 and kName3. Only the kName2 group should be left. Despite kName2 and |
| // kName3 groups both having "group-by-origin" execution mode, group kName3 |
| // should not have been left. |
| EXPECT_THAT( |
| storage->ClearOriginJoinedInterestGroups(kOriginB, {kName1, kName3}, |
| /*main_frame_origin=*/kOriginA), |
| testing::UnorderedElementsAre(kName2)); |
| EXPECT_THAT(GetInterestGroupSummary(*storage), |
| testing::UnorderedElementsAre( |
| // Origin B's groups that were joined on origin A. |
| "https://b.test;name1", "https://b.test;name3", |
| // Other groups. |
| "https://c.test;name1", "https://b.test;name4")); |
| |
| // Leave all of origin's B's interest groups joined from origin A. |
| EXPECT_THAT( |
| storage->ClearOriginJoinedInterestGroups(kOriginB, {}, |
| /*main_frame_origin=*/kOriginA), |
| testing::UnorderedElementsAre(kName1, kName3)); |
| EXPECT_THAT(GetInterestGroupSummary(*storage), |
| testing::UnorderedElementsAre("https://c.test;name1", |
| "https://b.test;name4")); |
| } |
| |
| // Make sure that ClearOriginJoinedInterestGroups() clears join, bid, and win |
| // history. |
| TEST_F(InterestGroupStorageTest, ClearOriginJoinedInterestGroupsClearsHistory) { |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://a.test")); |
| const char kName[] = "name"; |
| const blink::InterestGroupKey kGroupKey(kOrigin, kName); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->JoinInterestGroup(NewInterestGroup(kOrigin, kName), |
| kOrigin.GetURL()); |
| |
| // Increment each history count by 1. |
| storage->JoinInterestGroup(NewInterestGroup(kOrigin, kName), |
| kOrigin.GetURL()); |
| storage->RecordInterestGroupBids({kGroupKey}); |
| storage->RecordInterestGroupWin(kGroupKey, "{\"url\": \"https://ad.test\"}"); |
| |
| // Check the group is in the expected state. |
| std::optional<content::StorageInterestGroup> group = |
| storage->GetInterestGroup(kGroupKey); |
| ASSERT_TRUE(group); |
| EXPECT_EQ(2, group->bidding_browser_signals->join_count); |
| EXPECT_EQ(1, group->bidding_browser_signals->bid_count); |
| EXPECT_EQ(1u, group->bidding_browser_signals->prev_wins.size()); |
| |
| // Clear the group. |
| storage->ClearOriginJoinedInterestGroups(kOrigin, {}, |
| /*main_frame_origin=*/kOrigin); |
| EXPECT_FALSE(storage->GetInterestGroup(kGroupKey)); |
| |
| // Join the group again. |
| storage->JoinInterestGroup(NewInterestGroup(kOrigin, kName), |
| kOrigin.GetURL()); |
| |
| // Check that none of the history was retained. |
| group = storage->GetInterestGroup(kGroupKey); |
| ASSERT_TRUE(group); |
| EXPECT_EQ(1, group->bidding_browser_signals->join_count); |
| EXPECT_EQ(0, group->bidding_browser_signals->bid_count); |
| EXPECT_EQ(0u, group->bidding_browser_signals->prev_wins.size()); |
| } |
| |
| // Join 5 interest groups in the same origin, and one interest group in another |
| // origin. |
| // |
| // Fetch interest groups for update with a limit of 2 interest groups. Only 2 |
| // interest groups should be returned, and they should all belong to the first |
| // test origin. |
| // |
| // Then, fetch 100 groups for update. Only 5 should be returned. |
| TEST_F(InterestGroupStorageTest, GetInterestGroupsForUpdate) { |
| const url::Origin test_origin1 = |
| url::Origin::Create(GURL("https://owner1.example.com")); |
| const url::Origin test_origin2 = |
| url::Origin::Create(GURL("https://owner2.example.com")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| constexpr size_t kNumOrigin1Groups = 5, kSmallFetchGroups = 2, |
| kLargeFetchGroups = 100; |
| ASSERT_LT(kSmallFetchGroups, kNumOrigin1Groups); |
| ASSERT_GT(kLargeFetchGroups, kNumOrigin1Groups); |
| for (size_t i = 0; i < kNumOrigin1Groups; i++) { |
| storage->JoinInterestGroup( |
| NewInterestGroup(test_origin1, |
| base::StrCat({"example", base::NumberToString(i)})), |
| test_origin1.GetURL()); |
| } |
| storage->JoinInterestGroup(NewInterestGroup(test_origin2, "example"), |
| test_origin2.GetURL()); |
| |
| std::vector<InterestGroupUpdateParameter> update_infos = |
| storage->GetInterestGroupsForUpdate(test_origin1, |
| /*groups_limit=*/kSmallFetchGroups); |
| |
| GURL expected_update_url = test_origin1.GetURL().Resolve("/update_script.js"); |
| EXPECT_EQ(kSmallFetchGroups, update_infos.size()); |
| for (const auto& [ig_key, update_url, joining_origin] : update_infos) { |
| EXPECT_EQ(test_origin1, ig_key.owner); |
| EXPECT_EQ(update_url, expected_update_url); |
| EXPECT_EQ(test_origin1, joining_origin); |
| } |
| |
| update_infos = |
| storage->GetInterestGroupsForUpdate(test_origin1, |
| /*groups_limit=*/kLargeFetchGroups); |
| |
| EXPECT_EQ(kNumOrigin1Groups, update_infos.size()); |
| for (const auto& [ig_key, update_url, joining_origin] : update_infos) { |
| EXPECT_EQ(test_origin1, ig_key.owner); |
| EXPECT_EQ(update_url, expected_update_url); |
| EXPECT_EQ(test_origin1, joining_origin); |
| } |
| } |
| |
| TEST_F(InterestGroupStorageTest, BidCount) { |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->JoinInterestGroup(NewInterestGroup(test_origin, "example"), |
| test_origin.GetURL()); |
| blink::InterestGroupKey group_key(test_origin, "example"); |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| EXPECT_EQ(test_origin, origins[0]); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("example", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| |
| storage->RecordInterestGroupBids({group_key}); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("example", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->bid_count); |
| |
| storage->RecordInterestGroupBids({group_key}); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("example", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(2, interest_groups[0].bidding_browser_signals->bid_count); |
| } |
| |
| TEST_F(InterestGroupStorageTest, RecordsWins) { |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| const GURL ad1_url = GURL("http://owner.example.com/ad1"); |
| const GURL ad2_url = GURL("http://owner.example.com/ad2"); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| blink::InterestGroupKey group_key(test_origin, "example"); |
| |
| storage->JoinInterestGroup(NewInterestGroup(test_origin, "example"), |
| test_origin.GetURL()); |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| EXPECT_EQ(test_origin, origins[0]); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("example", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| |
| std::string ad1_json = "{url: '" + ad1_url.spec() + "'}"; |
| storage->RecordInterestGroupBids({group_key}); |
| storage->RecordInterestGroupWin(group_key, ad1_json); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("example", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->bid_count); |
| |
| // Add the second win *after* the first so we can check ordering. |
| task_environment().FastForwardBy(base::Seconds(1)); |
| std::string ad2_json = "{url: '" + ad2_url.spec() + "'}"; |
| storage->RecordInterestGroupBids({group_key}); |
| storage->RecordInterestGroupWin(group_key, ad2_json); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("example", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(2, interest_groups[0].bidding_browser_signals->bid_count); |
| EXPECT_EQ(2u, interest_groups[0].bidding_browser_signals->prev_wins.size()); |
| // Ad wins should be listed in reverse chronological order. |
| EXPECT_EQ(ad2_json, |
| interest_groups[0].bidding_browser_signals->prev_wins[0]->ad_json); |
| EXPECT_EQ(ad1_json, |
| interest_groups[0].bidding_browser_signals->prev_wins[1]->ad_json); |
| |
| // Try delete |
| storage->DeleteInterestGroupData( |
| base::BindLambdaForTesting([&test_origin]( |
| const blink::StorageKey& candidate) { |
| return candidate == blink::StorageKey::CreateFirstParty(test_origin); |
| }), |
| /*user_initiated_deletion=*/true); |
| |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(0u, origins.size()); |
| } |
| |
| TEST_F(InterestGroupStorageTest, RecordsDebugReportLockoutAndCooldown) { |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| const url::Origin test_origin2 = |
| url::Origin::Create(GURL("https://seller.example.com")); |
| std::vector<url::Origin> origins{test_origin, test_origin2}; |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| std::optional<DebugReportLockoutAndCooldowns> cooldowns = |
| storage->GetDebugReportLockoutAndCooldowns(origins); |
| ASSERT_TRUE(cooldowns.has_value()); |
| EXPECT_FALSE(cooldowns->lockout.has_value()); |
| EXPECT_TRUE(cooldowns->debug_report_cooldown_map.empty()); |
| |
| base::Time time = base::Time::Now(); |
| base::Time expected_time = base::Time::FromDeltaSinceWindowsEpoch( |
| time.ToDeltaSinceWindowsEpoch().CeilToMultiple(base::Hours(1))); |
| storage->RecordDebugReportLockout( |
| time, blink::features::kFledgeDebugReportLockout.Get()); |
| cooldowns = storage->GetDebugReportLockoutAndCooldowns(origins); |
| ASSERT_TRUE(cooldowns.has_value()); |
| ASSERT_TRUE(cooldowns->lockout.has_value()); |
| EXPECT_EQ(expected_time, cooldowns->lockout->starting_time); |
| EXPECT_EQ(blink::features::kFledgeDebugReportLockout.Get(), |
| cooldowns->lockout->duration); |
| EXPECT_TRUE(cooldowns->debug_report_cooldown_map.empty()); |
| |
| storage->RecordDebugReportCooldown(test_origin, time, |
| DebugReportCooldownType::kShortCooldown); |
| cooldowns = storage->GetDebugReportLockoutAndCooldowns(origins); |
| std::map<url::Origin, DebugReportCooldown> expected_cooldown_map; |
| expected_cooldown_map[test_origin] = DebugReportCooldown( |
| expected_time, DebugReportCooldownType::kShortCooldown); |
| ASSERT_TRUE(cooldowns.has_value()); |
| ASSERT_TRUE(cooldowns->lockout.has_value()); |
| EXPECT_EQ(expected_time, cooldowns->lockout->starting_time); |
| EXPECT_EQ(expected_cooldown_map, cooldowns->debug_report_cooldown_map); |
| expected_cooldown_map.clear(); |
| |
| // Ensure we get to a different hour, to get a different time. Also test |
| // customize lockout duration. |
| task_environment().FastForwardBy(base::Minutes(90)); |
| base::Time time2 = base::Time::Now(); |
| base::Time expected_time2 = base::Time::FromDeltaSinceWindowsEpoch( |
| time2.ToDeltaSinceWindowsEpoch().CeilToMultiple(base::Hours(1))); |
| storage->RecordDebugReportLockout(time2, /*duration=*/base::Days(90)); |
| storage->RecordDebugReportCooldown( |
| test_origin, time2, DebugReportCooldownType::kRestrictedCooldown); |
| storage->RecordDebugReportCooldown(test_origin2, time2, |
| DebugReportCooldownType::kShortCooldown); |
| cooldowns = storage->GetDebugReportLockoutAndCooldowns(origins); |
| expected_cooldown_map[test_origin] = DebugReportCooldown( |
| expected_time2, DebugReportCooldownType::kRestrictedCooldown); |
| expected_cooldown_map[test_origin2] = DebugReportCooldown( |
| expected_time2, DebugReportCooldownType::kShortCooldown); |
| ASSERT_TRUE(cooldowns.has_value()); |
| ASSERT_TRUE(cooldowns->lockout.has_value()); |
| EXPECT_EQ(expected_time2, cooldowns->lockout->starting_time); |
| EXPECT_EQ(base::Days(90), cooldowns->lockout->duration); |
| EXPECT_EQ(expected_cooldown_map, cooldowns->debug_report_cooldown_map); |
| } |
| |
| TEST_F(InterestGroupStorageTest, SetDebugReportLockoutUntilIGExpires) { |
| const char kName1[] = "name1"; |
| const char kName2[] = "name2"; |
| const char kName3[] = "name3"; |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://owner.test")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| const base::TimeDelta kDelta = base::Days(1); |
| |
| base::Time start = base::Time::Now(); |
| base::Time later = start + kDelta; |
| base::Time even_later = later + kDelta; |
| |
| // Already expired when joined. |
| storage->JoinInterestGroup( |
| blink::TestInterestGroupBuilder(kOrigin, kName1).SetExpiry(start).Build(), |
| kOrigin.GetURL()); |
| |
| // Expires when time reaches `later`. |
| storage->JoinInterestGroup( |
| blink::TestInterestGroupBuilder(kOrigin, kName2).SetExpiry(later).Build(), |
| kOrigin.GetURL()); |
| |
| // Expires when time reaches `even_later`. |
| storage->JoinInterestGroup(blink::TestInterestGroupBuilder(kOrigin, kName3) |
| .SetExpiry(even_later) |
| .Build(), |
| kOrigin.GetURL()); |
| |
| std::optional<DebugReportLockoutAndCooldowns> lockout_and_cooldowns = |
| storage->GetDebugReportLockoutAndAllCooldowns(); |
| ASSERT_TRUE(lockout_and_cooldowns.has_value()); |
| ASSERT_FALSE(lockout_and_cooldowns->lockout.has_value()); |
| |
| storage->SetDebugReportLockoutUntilIGExpires(); |
| lockout_and_cooldowns = storage->GetDebugReportLockoutAndAllCooldowns(); |
| ASSERT_TRUE(lockout_and_cooldowns.has_value()); |
| ASSERT_TRUE(lockout_and_cooldowns->lockout.has_value()); |
| base::Time expected_starting_time = base::Time::FromDeltaSinceWindowsEpoch( |
| start.ToDeltaSinceWindowsEpoch().CeilToMultiple(base::Hours(1))); |
| EXPECT_EQ(expected_starting_time, |
| lockout_and_cooldowns->lockout->starting_time); |
| EXPECT_EQ(even_later - expected_starting_time, |
| lockout_and_cooldowns->lockout->duration); |
| |
| // All IGs joined before has already expired. |
| task_environment().FastForwardBy(base::Days(3)); |
| storage->SetDebugReportLockoutUntilIGExpires(); |
| lockout_and_cooldowns = storage->GetDebugReportLockoutAndAllCooldowns(); |
| ASSERT_TRUE(lockout_and_cooldowns.has_value()); |
| ASSERT_FALSE(lockout_and_cooldowns->lockout.has_value()); |
| } |
| |
| TEST_F(InterestGroupStorageTest, DeleteExpiredDebugReportCooldown) { |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| const url::Origin test_origin2 = |
| url::Origin::Create(GURL("https://seller.example.com")); |
| std::vector<url::Origin> origins{test_origin, test_origin2}; |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| base::Time time = base::Time::Now(); |
| base::Time expected_time = base::Time::FromDeltaSinceWindowsEpoch( |
| time.ToDeltaSinceWindowsEpoch().CeilToMultiple(base::Hours(1))); |
| storage->RecordDebugReportCooldown(test_origin, time, |
| DebugReportCooldownType::kShortCooldown); |
| storage->RecordDebugReportCooldown(test_origin2, time, |
| DebugReportCooldownType::kShortCooldown); |
| std::optional<DebugReportLockoutAndCooldowns> cooldowns = |
| storage->GetDebugReportLockoutAndCooldowns(origins); |
| std::map<url::Origin, DebugReportCooldown> expected_cooldown_map; |
| expected_cooldown_map[test_origin] = DebugReportCooldown( |
| expected_time, DebugReportCooldownType::kShortCooldown); |
| expected_cooldown_map[test_origin2] = DebugReportCooldown( |
| expected_time, DebugReportCooldownType::kShortCooldown); |
| ASSERT_TRUE(cooldowns.has_value()); |
| EXPECT_EQ(expected_cooldown_map, cooldowns->debug_report_cooldown_map); |
| |
| // Fast-forward past kFledgeDebugReportShortCooldown so that the cooldown will |
| // expire. Fast-forward extra time to make sure the cooldown expires, |
| // because the starting_time is ceiled to its nearest hour. |
| task_environment().FastForwardBy( |
| blink::features::kFledgeDebugReportShortCooldown.Get() + expected_time - |
| time); |
| // If maintenance has not been triggered yet, the cooldown table will not be |
| // updated. |
| cooldowns = storage->GetDebugReportLockoutAndCooldowns(origins); |
| ASSERT_TRUE(cooldowns.has_value()); |
| EXPECT_EQ(expected_cooldown_map, cooldowns->debug_report_cooldown_map); |
| // Trigger scheduling of the next maintenance. |
| storage->GetAllInterestGroupOwners(); |
| // Allow enough idle time to trigger maintenance. |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod + |
| base::Seconds(1)); |
| |
| cooldowns = storage->GetDebugReportLockoutAndCooldowns(origins); |
| ASSERT_TRUE(cooldowns.has_value()); |
| EXPECT_TRUE(cooldowns->debug_report_cooldown_map.empty()); |
| } |
| |
| TEST_F(InterestGroupStorageTest, DeleteAllDebugReportCooldowns) { |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| const url::Origin test_origin2 = |
| url::Origin::Create(GURL("https://seller.example.com")); |
| std::vector<url::Origin> origins{test_origin, test_origin2}; |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| base::Time time = base::Time::Now(); |
| base::Time expected_time = base::Time::FromDeltaSinceWindowsEpoch( |
| time.ToDeltaSinceWindowsEpoch().CeilToMultiple(base::Hours(1))); |
| storage->RecordDebugReportCooldown(test_origin, time, |
| DebugReportCooldownType::kShortCooldown); |
| storage->RecordDebugReportCooldown(test_origin2, time, |
| DebugReportCooldownType::kShortCooldown); |
| std::optional<DebugReportLockoutAndCooldowns> cooldowns = |
| storage->GetDebugReportLockoutAndCooldowns(origins); |
| std::map<url::Origin, DebugReportCooldown> expected_cooldown_map; |
| expected_cooldown_map[test_origin] = DebugReportCooldown( |
| expected_time, DebugReportCooldownType::kShortCooldown); |
| expected_cooldown_map[test_origin2] = DebugReportCooldown( |
| expected_time, DebugReportCooldownType::kShortCooldown); |
| ASSERT_TRUE(cooldowns.has_value()); |
| EXPECT_EQ(expected_cooldown_map, cooldowns->debug_report_cooldown_map); |
| |
| storage->DeleteAllDebugReportCooldowns(); |
| |
| cooldowns = storage->GetDebugReportLockoutAndCooldowns(origins); |
| ASSERT_TRUE(cooldowns.has_value()); |
| EXPECT_TRUE(cooldowns->debug_report_cooldown_map.empty()); |
| } |
| |
| // TODO (b/356654297) Add tests for selectableBuyerAndSellerReportingIds, |
| // when k-anon is implemented. |
| TEST_F(InterestGroupStorageTest, UpdatesAdKAnonymity) { |
| url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| GURL ad2_url = GURL("https://owner.example.com/ad2"); |
| GURL ad3_url = GURL("https://owner.example.com/ad3"); |
| |
| InterestGroup g = NewInterestGroup(test_origin, "name"); |
| g.ads.emplace(); |
| g.ads->emplace_back( |
| ad1_url, "metadata1", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid1", |
| /*buyer_and_seller_reporting_id=*/"shrid1", |
| /*selectable_buyer_and_seller_reporting_ids=*/ |
| std::vector<std::string>{"selectable_id1", "selectable_id2"}); |
| g.ads->emplace_back(ad2_url, "metadata2", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid2", |
| /*buyer_and_seller_reporting_id=*/std::nullopt); |
| g.ad_components.emplace(); |
| g.ad_components->push_back( |
| blink::InterestGroup::Ad(ad1_url, "component_metadata1")); |
| g.ad_components->push_back( |
| blink::InterestGroup::Ad(ad3_url, "component_metadata3")); |
| |
| std::string kanon_bid1 = blink::HashedKAnonKeyForAdBid(g, ad1_url.spec()); |
| std::string kanon_report1 = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[0], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| std::string kanon_bid2 = blink::HashedKAnonKeyForAdBid(g, ad2_url.spec()); |
| std::string kanon_report2 = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[1], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| std::string kanon_component_1 = blink::HashedKAnonKeyForAdComponentBid( |
| g.ad_components.value()[0].render_url()); |
| std::string kanon_component_2 = blink::HashedKAnonKeyForAdComponentBid( |
| g.ad_components.value()[1].render_url()); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| |
| EXPECT_EQ(0u, groups.size()); |
| |
| storage->JoinInterestGroup(g, GURL("https://owner.example.com/join")); |
| |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| |
| std::vector<std::string> expected_positive_keys = {}; |
| |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_keys)); |
| |
| base::Time update_time = base::Time::Now(); |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g.owner, g.name), |
| {kanon_bid1, kanon_report1}, update_time, |
| /*replace_existing_values*/ true); |
| expected_positive_keys = {kanon_bid1, kanon_report1}; |
| |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_keys)); |
| EXPECT_THAT(groups[0].last_k_anon_updated, update_time); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| // The new update for the interest group will override the old values because |
| // we set replace_existing_values = true. |
| update_time = base::Time::Now(); |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g.owner, g.name), |
| {kanon_bid1, kanon_bid2, kanon_report2}, |
| update_time, |
| /*replace_existing_values*/ true); |
| expected_positive_keys = {kanon_bid1, kanon_bid2, kanon_report2}; |
| |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_keys)); |
| EXPECT_THAT(groups[0].last_k_anon_updated, update_time); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| // Try doing a non-replacing update with the same update time as is already in |
| // the database. It should succeed. |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g.owner, g.name), |
| {kanon_report1}, update_time, |
| /*replace_existing_values*/ false); |
| expected_positive_keys.push_back(kanon_report1); |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_keys)); |
| EXPECT_THAT(groups[0].last_k_anon_updated, update_time); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| // Try doing a non-replacing update with a later update time. It should |
| // succeed. |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g.owner, g.name), |
| {kanon_component_1, kanon_bid2}, |
| update_time + base::Seconds(10), |
| /*replace_existing_values*/ false); |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| expected_positive_keys.push_back(kanon_component_1); |
| |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_keys)); |
| EXPECT_THAT(groups[0].last_k_anon_updated, update_time); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| // Try doing a non-replacing update with an earlier update time. We should |
| // still get the same values as before. |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g.owner, g.name), |
| {kanon_component_2}, |
| update_time - base::Seconds(10), |
| /*replace_existing_values*/ false); |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_keys)); |
| EXPECT_THAT(groups[0].last_k_anon_updated, update_time); |
| |
| // An update with replace_existing_values = true and an earlier `update_time` |
| // shouldn't result in any changed values. |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g.owner, g.name), |
| {kanon_component_2}, |
| update_time - base::Seconds(10), |
| /*replace_existing_values*/ true); |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_keys)); |
| EXPECT_THAT(groups[0].last_k_anon_updated, update_time); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| // A replacing update following non-replacing updates should update the |
| // last_k_anon_updated and values for all k-anon. |
| update_time = base::Time::Now(); |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g.owner, g.name), |
| {kanon_bid1}, update_time, |
| /*replace_existing_values*/ true); |
| expected_positive_keys = {kanon_bid1}; |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAreArray(expected_positive_keys)); |
| EXPECT_THAT(groups[0].last_k_anon_updated, update_time); |
| } |
| |
| TEST_F(InterestGroupStorageTest, |
| UpdatesAdKAnonymityWithMultipleInterestGroups) { |
| url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| GURL ad2_url = GURL("https://owner.example.com/ad2"); |
| GURL ad3_url = GURL("https://owner.example.com/ad3"); |
| |
| InterestGroup g1 = NewInterestGroup(test_origin, "name"); |
| g1.ads.emplace(); |
| g1.ads->emplace_back(ad1_url, "metadata1"); |
| g1.ad_components.emplace(); |
| g1.ad_components->emplace_back(ad1_url, "component_metadata1"); |
| g1.ad_components->emplace_back(ad3_url, "component_metadata3"); |
| g1.expiry = base::Time::Now() + blink::MaxInterestGroupLifetimeForMetadata(); |
| |
| InterestGroup g2 = g1; |
| g2.ads->emplace_back(ad2_url, "metadata2"); |
| g2.name = "name 2"; |
| g2.expiry = base::Time::Now() + blink::MaxInterestGroupLifetimeForMetadata() + |
| base::Hours(1); |
| |
| InterestGroup g3 = g1; |
| g3.ad_components->clear(); |
| g3.name = "name 3"; |
| g3.expiry = base::Time::Now() + blink::MaxInterestGroupLifetimeForMetadata() + |
| base::Hours(2); |
| |
| std::string k_anon_bid_key_1 = |
| blink::HashedKAnonKeyForAdBid(g1, ad1_url.spec()); |
| std::string k_anon_bid_key_2 = |
| blink::HashedKAnonKeyForAdBid(g2, ad2_url.spec()); |
| std::string k_anon_component_key_1 = |
| blink::HashedKAnonKeyForAdComponentBid(ad1_url); |
| std::string k_anon_component_key_3 = |
| blink::HashedKAnonKeyForAdComponentBid(ad3_url); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| // A true k-anonymity value should be returned with just one interest group. |
| base::Time update_time1(base::Time::Now()); |
| storage->JoinInterestGroup(g1, GURL("example.com")); |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g1.owner, g1.name), |
| {k_anon_bid_key_1}, update_time1, |
| /*replace_existing_values*/ true); |
| std::vector<StorageInterestGroup> returned_groups = |
| storage->GetInterestGroupsForOwner(g1.owner); |
| EXPECT_EQ(returned_groups.size(), 1u); |
| EXPECT_THAT(returned_groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid_key_1)); |
| EXPECT_EQ(returned_groups[0].last_k_anon_updated, update_time1); |
| |
| task_environment().FastForwardBy(base::Hours(1)); |
| |
| // The second interest group has not had a k-anon value update yet, so it |
| // will not be returned with any k-anon values, despite it sharing a key |
| // with the first group. |
| storage->JoinInterestGroup(g2, GURL("example.com")); |
| returned_groups = storage->GetInterestGroupsForOwner(g1.owner); |
| { |
| auto expected_interest_group_matcher = testing::UnorderedElementsAre( |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid_key_1)), |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, update_time1), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g1.owner), |
| Field("name", &InterestGroup::name, g1.name)))), |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::IsEmpty()), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g2.owner), |
| Field("name", &InterestGroup::name, g2.name))))); |
| EXPECT_THAT(returned_groups, expected_interest_group_matcher); |
| } |
| |
| // Updating a k-anon value for an ad only in the second interest group should |
| // not affect the returned k-anonimity values for the first group. |
| base::Time update_time2(base::Time::Now()); |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g2.owner, g2.name), |
| {k_anon_bid_key_2}, update_time2, |
| /*replace_existing_values*/ true); |
| returned_groups = storage->GetInterestGroupsForOwner(g1.owner); |
| { |
| auto expected_interest_group_matcher = testing::UnorderedElementsAre( |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid_key_1)), |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, update_time1), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g1.owner), |
| Field("name", &InterestGroup::name, g1.name)))), |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid_key_2)), |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, update_time2), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g2.owner), |
| Field("name", &InterestGroup::name, g2.name))))); |
| EXPECT_THAT(returned_groups, expected_interest_group_matcher); |
| } |
| |
| task_environment().FastForwardBy(base::Hours(1)); |
| |
| // Updating a k-anon value for an ad in both interest groups should affect |
| // only the interest group for which we are doing the update. |
| base::Time update_time3(base::Time::Now()); |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g1.owner, g1.name), |
| {k_anon_component_key_3}, update_time3, |
| /*replace_existing_values*/ true); |
| returned_groups = storage->GetInterestGroupsForOwner(g1.owner); |
| { |
| auto expected_interest_group_matcher = testing::UnorderedElementsAre( |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_component_key_3)), |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, update_time3), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g1.owner), |
| Field("name", &InterestGroup::name, g1.name)))), |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid_key_2)), |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, update_time2), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g2.owner), |
| Field("name", &InterestGroup::name, g2.name))))); |
| EXPECT_THAT(returned_groups, expected_interest_group_matcher); |
| } |
| |
| // After joining a third interest group that shares k-anon keys with the |
| // previously joined groups, the third interest group should be not be |
| // returned with any k-anon keys set since the third group has not received a |
| // k-anon update. |
| storage->JoinInterestGroup(g3, GURL("example.com")); |
| returned_groups = storage->GetInterestGroupsForOwner(g1.owner); |
| { |
| auto expected_interest_group_matcher = testing::UnorderedElementsAre( |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_component_key_3)), |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, update_time3), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g1.owner), |
| Field("name", &InterestGroup::name, g1.name)))), |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid_key_2)), |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, update_time2), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g2.owner), |
| Field("name", &InterestGroup::name, g2.name)))), |
| testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::IsEmpty()), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g3.owner), |
| Field("name", &InterestGroup::name, g3.name))))); |
| EXPECT_THAT(returned_groups, expected_interest_group_matcher); |
| } |
| |
| // Check that the k_anon_bid1 is unaffected by the expiration of the other |
| // values (because it's attached to a newer interest group, g3). |
| storage->UpdateKAnonymity(blink::InterestGroupKey(g3.owner, g3.name), |
| {k_anon_bid_key_1}, update_time3, |
| /*replace_existing_values*/ true); |
| task_environment().FastForwardBy( |
| blink::MaxInterestGroupLifetimeForMetadata() - base::Hours(1)); |
| |
| returned_groups = storage->GetInterestGroupsForOwner(g1.owner); |
| { |
| auto expected_interest_group_matcher = |
| testing::UnorderedElementsAre(testing::AllOf( |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid_key_1)), |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, update_time3), |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("owner", &InterestGroup::owner, g3.owner), |
| Field("name", &InterestGroup::name, g3.name))))); |
| EXPECT_EQ(returned_groups.size(), 1u); |
| EXPECT_THAT(returned_groups, expected_interest_group_matcher); |
| } |
| } |
| |
| TEST_F(InterestGroupStorageTest, KAnonDataExpires) { |
| GURL update_url("https://owner.example.com/groupUpdate"); |
| url::Origin test_origin = url::Origin::Create(update_url); |
| const std::string name = "name"; |
| // We make the ad urls equal to the name key and update urls to verify the |
| // database stores them separately. |
| GURL ad1_url = GURL("https://owner.example.com/groupUpdate"); |
| GURL ad2_url = GURL("https://owner.example.com/name"); |
| |
| InterestGroup g = NewInterestGroup(test_origin, name); |
| blink::InterestGroupKey interest_group_key(g.owner, g.name); |
| g.ads.emplace(); |
| g.ads->push_back(blink::InterestGroup::Ad(ad1_url, "metadata1")); |
| g.ad_components.emplace(); |
| g.ad_components->push_back( |
| blink::InterestGroup::Ad(ad2_url, "component_metadata2")); |
| g.update_url = update_url; |
| g.expiry = base::Time::Now() + base::Days(1); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->JoinInterestGroup(g, GURL("https://owner.example.com/join")); |
| |
| // Update the k-anonymity data. |
| base::Time update_kanon_time = base::Time::Now(); |
| std::string ad1_bid_kanon = blink::HashedKAnonKeyForAdBid(g, ad1_url.spec()); |
| std::string ad1_report_kanon = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads.value()[0], |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| std::string ad2_bid_kanon = blink::HashedKAnonKeyForAdComponentBid(ad2_url); |
| storage->UpdateKAnonymity(interest_group_key, |
| {ad1_bid_kanon, ad1_report_kanon, ad2_bid_kanon}, |
| update_kanon_time, |
| /*replace_existing_values*/ true); |
| |
| // Check k-anonymity data was correctly set. |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_THAT(groups[0].hashed_kanon_keys, |
| testing::UnorderedElementsAre(ad1_bid_kanon, ad1_report_kanon, |
| ad2_bid_kanon)); |
| |
| // Fast-forward past interest group expiration. |
| task_environment().FastForwardBy(base::Days(2)); |
| |
| // Interest group should no longer exist. |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(0u, groups.size()); |
| |
| // Join again and expect empty kanon values. |
| g.expiry = base::Time::Now() + base::Days(1); |
| storage->JoinInterestGroup(g, GURL("https://owner.example.com/join3")); |
| |
| groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, groups.size()); |
| EXPECT_TRUE(groups[0].hashed_kanon_keys.empty()); |
| } |
| |
| TEST_F(InterestGroupStorageTest, ClickinessDelete) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| const url::Origin kViewClickEligibleOrigin3 = |
| url::Origin::Create(GURL("https://view-click.eligible3.test")); |
| const url::Origin kViewClickProviderOrigin3 = |
| url::Origin::Create(GURL("https://view-click.provider3.test")); |
| |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin2; |
| record.eligible_origins = {kViewClickEligibleOrigin2}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin3; |
| record.eligible_origins = {kViewClickEligibleOrigin3}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| |
| EXPECT_EQ(true, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin1, kViewClickEligibleOrigin1)); |
| EXPECT_EQ(true, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin2, kViewClickEligibleOrigin2)); |
| EXPECT_EQ(true, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin3, kViewClickEligibleOrigin3)); |
| |
| // Try delete for provider origins 1 and 2, eligible origin 3, as |
| // non-user-initiated. Provider origins 1 and 2 should be deleted. |
| auto predicate = |
| base::BindLambdaForTesting([&](const blink::StorageKey& candidate) { |
| return candidate == blink::StorageKey::CreateFirstParty( |
| kViewClickProviderOrigin1) || |
| candidate == blink::StorageKey::CreateFirstParty( |
| kViewClickProviderOrigin2) || |
| candidate == blink::StorageKey::CreateFirstParty( |
| kViewClickEligibleOrigin3); |
| }); |
| storage->DeleteInterestGroupData(predicate, |
| /*user_initiated_deletion=*/false); |
| EXPECT_EQ(false, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin1, kViewClickEligibleOrigin1)); |
| EXPECT_EQ(false, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin2, kViewClickEligibleOrigin2)); |
| EXPECT_EQ(true, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin3, kViewClickEligibleOrigin3)); |
| |
| // Try the same predicate with `user_initiated_deletion` set to true; this |
| // should delete everything. |
| storage->DeleteInterestGroupData(predicate, /*user_initiated_deletion=*/true); |
| EXPECT_EQ(false, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin3, kViewClickEligibleOrigin3)); |
| } |
| |
| // Null callback in non-user-initiated mode doesn't crash. |
| TEST_F(InterestGroupStorageTest, ClickinessDelete2) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| |
| storage->DeleteInterestGroupData(base::NullCallback(), |
| /*user_initiated_deletion=*/false); |
| EXPECT_EQ(false, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin1, kViewClickEligibleOrigin1)); |
| } |
| |
| enum class GroupLifetime { |
| k30Day, |
| k90Day, |
| }; |
| |
| class InterestGroupStorageDualLifetimeTest |
| : public InterestGroupStorageTest, |
| public ::testing::WithParamInterface<GroupLifetime> { |
| public: |
| void SetUp() override { |
| InterestGroupStorageTest::SetUp(); |
| switch (GetParam()) { |
| case GroupLifetime::k30Day: |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| blink::features::kFledgeMaxGroupLifetimeFeature, |
| {{"fledge_max_group_lifetime", "30d"}}); |
| break; |
| case GroupLifetime::k90Day: |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| blink::features::kFledgeMaxGroupLifetimeFeature, |
| {{"fledge_max_group_lifetime", "90d"}}); |
| break; |
| } |
| } |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, ViewClickStoreRetrieve_Basic) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // No counts for negative groups. |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_NegativeGroup) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| g.update_url = std::nullopt; |
| g.additional_bid_key.emplace(); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // No providers defaults to IG origin. |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_NullProviders) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kClick; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kClick; |
| record.providing_origin = kViewClickProviderOrigin2; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickEligibleOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = std::nullopt; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| // These are view, not clicks. |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // Empty providers defaults to IG origin. |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_EmptyProviders) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kClick; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kClick; |
| record.providing_origin = kViewClickProviderOrigin2; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| { |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickEligibleOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| } |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| // These are view, not clicks. |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_BasicByGroupKey) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::optional<StorageInterestGroup> group = storage->GetInterestGroup( |
| blink::InterestGroupKey(kViewClickEligibleOrigin1, "cars")); |
| ASSERT_TRUE(group); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| group->bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, ViewClickStoreRetrieve_NoEvents) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_NoEventsNoProvider) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_EligibleDefaultsToProvider) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g = NewInterestGroup(kViewClickProviderOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickProviderOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_ProviderNotInEligibleOrigins) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g = NewInterestGroup(kViewClickProviderOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickProviderOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_BasicWithTwoEvents) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_BucketsTime) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record_view; |
| record_view.type = AdAuctionEventRecord::Type::kView; |
| record_view.providing_origin = kViewClickProviderOrigin1; |
| record_view.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_view.IsValid()); |
| |
| AdAuctionEventRecord record_click; |
| record_click.type = AdAuctionEventRecord::Type::kClick; |
| record_click.providing_origin = kViewClickProviderOrigin1; |
| record_click.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_click.IsValid()); |
| |
| // Timeline as follows, in time before final time |
| // (d: day, w: week, h: hour, V: view, C: click, F: final time): |
| // |
| // 90d 30d 1w 1d 1h F |
| // 1V,1C | 2V,1C | 0V,2C | 1V,0C | 2V,2C | 1V,2C | |
| |
| base::Time original_maintenance_time = |
| storage->GetLastMaintenanceTimeForTesting(); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_click); |
| |
| task_environment().FastForwardBy(base::Days(90) - base::Days(30)); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_view); |
| |
| storage->RecordViewClick(record_click); |
| |
| task_environment().FastForwardBy(base::Days(30) - base::Days(7)); |
| |
| storage->RecordViewClick(record_click); |
| storage->RecordViewClick(record_click); |
| |
| task_environment().FastForwardBy(base::Days(7) - base::Days(1)); |
| |
| storage->RecordViewClick(record_view); |
| |
| task_environment().FastForwardBy(base::Days(1) - base::Hours(1)); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_view); |
| |
| storage->RecordViewClick(record_click); |
| storage->RecordViewClick(record_click); |
| |
| task_environment().FastForwardBy(base::Hours(1)); |
| |
| storage->RecordViewClick(record_view); |
| |
| storage->RecordViewClick(record_click); |
| storage->RecordViewClick(record_click); |
| |
| // Check that maintenance (and therefore compaction) has occurred. |
| // (The ViewClickStoreRetrieve_BucketsTimeNoMaintenance test case below checks |
| // that reading non-compacted past events also works). |
| EXPECT_NE(original_maintenance_time, |
| storage->GetLastMaintenanceTimeForTesting()); |
| |
| // Fast forward one more second so that events aren't on the exact boundaries |
| // between time buckets. Each event will go to the next older time bucket, |
| // and the oldest events fall out of reporting. |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(3, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(4, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(4, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 6 : 4, |
| view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(2, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(4, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(4, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(6, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 7 : 6, |
| view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // Like ViewClickStoreRetrieve_BucketsTime, but destroys and recreates the |
| // storage between fast forwards such that maintenance never runs. |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_BucketsTimeNoMaintenance) { |
| AdAuctionEventRecord record_view; |
| record_view.type = AdAuctionEventRecord::Type::kView; |
| record_view.providing_origin = kViewClickProviderOrigin1; |
| record_view.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_view.IsValid()); |
| |
| AdAuctionEventRecord record_click; |
| record_click.type = AdAuctionEventRecord::Type::kClick; |
| record_click.providing_origin = kViewClickProviderOrigin1; |
| record_click.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_click.IsValid()); |
| |
| // Timeline as follows, in time before final time |
| // (d: day, w: week, h: hour, V: view, C: click, F: final time): |
| // |
| // 90d 30d 1w 1d 1h F |
| // 1V,1C | 2V,1C | 0V,2C | 1V,0C | 2V,2C | 1V,2C | |
| |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_click); |
| |
| // No maintenance run. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| } |
| |
| task_environment().FastForwardBy(base::Days(90) - base::Days(30)); |
| |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_view); |
| |
| storage->RecordViewClick(record_click); |
| |
| // No maintenance run. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| } |
| |
| task_environment().FastForwardBy(base::Days(30) - base::Days(7)); |
| |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->RecordViewClick(record_click); |
| storage->RecordViewClick(record_click); |
| |
| // No maintenance run. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| } |
| |
| task_environment().FastForwardBy(base::Days(7) - base::Days(1)); |
| |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->RecordViewClick(record_view); |
| |
| // No maintenance run. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| } |
| |
| task_environment().FastForwardBy(base::Days(1) - base::Hours(1)); |
| |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_view); |
| |
| storage->RecordViewClick(record_click); |
| storage->RecordViewClick(record_click); |
| |
| // No maintenance run. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| } |
| |
| task_environment().FastForwardBy(base::Hours(1)); |
| |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| storage->RecordViewClick(record_view); |
| |
| storage->RecordViewClick(record_click); |
| storage->RecordViewClick(record_click); |
| |
| // No maintenance run. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| } |
| |
| // Fast forward one more second so that events aren't on the exact boundaries |
| // between time buckets. Each event will go to the next older time bucket, |
| // and the oldest events fall out of reporting. |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(1, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(3, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(4, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(4, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 6 : 4, |
| view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(2, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(4, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(4, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(6, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(GetParam() == GroupLifetime::k90Day ? 7 : 6, |
| view_and_click_counts->click_counts->past_90_days); |
| |
| // No maintenance run. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_NotEligibleOrigin) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin2}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_ProviderMismatch) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin2}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_MultipleProviders) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record_provider_1; |
| record_provider_1.type = AdAuctionEventRecord::Type::kView; |
| record_provider_1.providing_origin = kViewClickProviderOrigin1; |
| record_provider_1.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_provider_1.IsValid()); |
| |
| storage->RecordViewClick(record_provider_1); |
| |
| AdAuctionEventRecord record_view_provider_2; |
| record_view_provider_2.type = AdAuctionEventRecord::Type::kView; |
| record_view_provider_2.providing_origin = kViewClickProviderOrigin1; |
| record_view_provider_2.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_view_provider_2.IsValid()); |
| |
| AdAuctionEventRecord record_click_provider_2; |
| record_click_provider_2.type = AdAuctionEventRecord::Type::kClick; |
| record_click_provider_2.providing_origin = kViewClickProviderOrigin1; |
| record_click_provider_2.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_click_provider_2.IsValid()); |
| |
| storage->RecordViewClick(record_view_provider_2); |
| storage->RecordViewClick(record_click_provider_2); |
| |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = { |
| {kViewClickProviderOrigin1, kViewClickProviderOrigin2}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(2, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_RateLimited) { |
| // Max of 10 views, 10 clicks, every 20 seconds, per (providing origin, |
| // eligible origin). |
| constexpr int kMaxEvents = 10; |
| constexpr base::TimeDelta kMaxEventsDelta = base::Seconds(20); |
| |
| AdAuctionEventRecord record_view; |
| record_view.type = AdAuctionEventRecord::Type::kView; |
| record_view.providing_origin = kViewClickProviderOrigin1; |
| record_view.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_view.IsValid()); |
| |
| AdAuctionEventRecord record_click; |
| record_click.type = AdAuctionEventRecord::Type::kClick; |
| record_click.providing_origin = kViewClickProviderOrigin1; |
| record_click.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_click.IsValid()); |
| |
| AdAuctionEventRecord record_view_other_provider; |
| record_view_other_provider.type = AdAuctionEventRecord::Type::kView; |
| record_view_other_provider.providing_origin = kViewClickProviderOrigin2; |
| record_view_other_provider.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_view_other_provider.IsValid()); |
| |
| AdAuctionEventRecord record_click_other_eligible_origin; |
| record_click_other_eligible_origin.type = AdAuctionEventRecord::Type::kClick; |
| record_click_other_eligible_origin.providing_origin = |
| kViewClickProviderOrigin1; |
| record_click_other_eligible_origin.eligible_origins = { |
| kViewClickEligibleOrigin2}; |
| ASSERT_TRUE(record_click_other_eligible_origin.IsValid()); |
| |
| // Which gets rate limited first, views, or clicks. Checking what happens in |
| // each scenario ensures that one of views and clicks getting rate limited |
| // doesn't mean the other one will be rate limited. |
| enum class Scenario { |
| kViewFirst = 0, |
| kClickFirst, |
| }; |
| for (Scenario scenario : {Scenario::kViewFirst, Scenario::kClickFirst}) { |
| SCOPED_TRACE(static_cast<int>(scenario)); |
| |
| // First, create the storage, and join an interest group. |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| InterestGroup g1 = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g1.view_and_click_counts_providers = { |
| {kViewClickProviderOrigin1, kViewClickProviderOrigin2}}; |
| g1.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g1, GURL("https://joining-site.test")); |
| |
| InterestGroup g2 = NewInterestGroup(kViewClickEligibleOrigin2, "shoes"); |
| g2.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g2.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g2, GURL("https://joining-site.test")); |
| |
| // Now record kMaxEvents + 1 events of the first type. Only kMaxEvents get |
| // recorded. |
| for (int i = 0; i < kMaxEvents + 1; i++) { |
| switch (scenario) { |
| case Scenario::kViewFirst: |
| storage->RecordViewClick(record_view); |
| break; |
| case Scenario::kClickFirst: |
| storage->RecordViewClick(record_click); |
| break; |
| } |
| } |
| |
| { |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| int view_count = 0; |
| int click_count = 0; |
| switch (scenario) { |
| case Scenario::kViewFirst: |
| view_count = kMaxEvents; |
| break; |
| case Scenario::kClickFirst: |
| click_count = kMaxEvents; |
| break; |
| } |
| |
| EXPECT_EQ(view_count, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(view_count, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(view_count, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(view_count, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(view_count, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(click_count, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(click_count, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(click_count, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(click_count, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(click_count, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // Now record kMaxEvents + 1 events of the second type. Only kMaxEvents get |
| // recorded. |
| for (int i = 0; i < kMaxEvents + 1; i++) { |
| switch (scenario) { |
| case Scenario::kViewFirst: |
| storage->RecordViewClick(record_click); |
| break; |
| case Scenario::kClickFirst: |
| storage->RecordViewClick(record_view); |
| break; |
| } |
| } |
| |
| { |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // (kViewClickEligibleOrigin1, kViewClickProviderOrigin1)'s currently rate |
| // limited, but events for other eligible origins and providers should |
| // be counted. |
| storage->RecordViewClick(record_view_other_provider); |
| storage->RecordViewClick(record_click_other_eligible_origin); |
| |
| { |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(kMaxEvents + 1, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(kMaxEvents + 1, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(kMaxEvents + 1, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(kMaxEvents + 1, |
| view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(kMaxEvents + 1, |
| view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(kMaxEvents, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| { |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin2); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // Finally advance time. (kViewClickProviderOrigin1, |
| // kViewClickEligibleOrigin1) should no longer be rate-limited. |
| task_environment().FastForwardBy(kMaxEventsDelta + base::Microseconds(1)); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_click); |
| |
| { |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(kMaxEvents + 2, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(kMaxEvents + 2, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(kMaxEvents + 2, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(kMaxEvents + 2, |
| view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(kMaxEvents + 2, |
| view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(kMaxEvents + 1, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(kMaxEvents + 1, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(kMaxEvents + 1, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(kMaxEvents + 1, |
| view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(kMaxEvents + 1, |
| view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // Delete the database in case we loop again, creating the database from |
| // another .sql file. |
| storage.reset(); |
| base::DeleteFile(db_path()); |
| } |
| } |
| |
| TEST_P(InterestGroupStorageDualLifetimeTest, |
| ViewClickStoreRetrieve_TwoEligibleOrigins) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record; |
| record.type = AdAuctionEventRecord::Type::kView; |
| record.providing_origin = kViewClickProviderOrigin1; |
| record.eligible_origins = {kViewClickEligibleOrigin1, |
| kViewClickEligibleOrigin2}; |
| ASSERT_TRUE(record.IsValid()); |
| storage->RecordViewClick(record); |
| |
| InterestGroup g_cars = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g_cars.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g_cars.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g_cars, GURL("https://joining-site.test")); |
| |
| InterestGroup g_shoes = NewInterestGroup(kViewClickEligibleOrigin2, "shoes"); |
| g_shoes.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g_shoes.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g_shoes, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups_cars = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups_cars.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts_cars = |
| groups_cars[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts_cars->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts_cars->click_counts->past_90_days); |
| |
| std::vector<StorageInterestGroup> groups_shoes = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups_shoes.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts_shoes = |
| groups_shoes[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_hour); |
| EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_day); |
| EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_week); |
| EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_30_days); |
| EXPECT_EQ(1, view_and_click_counts_shoes->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts_shoes->click_counts->past_90_days); |
| } |
| |
| // Make sure that nothing goes wrong when the clock is rolled back, making |
| // previously compacted events look fresh again. |
| TEST_P(InterestGroupStorageDualLifetimeTest, ClickinessCompactTimeRevert) { |
| base::Time epoch = base::Time::FromDeltaSinceWindowsEpoch(base::TimeDelta()); |
| |
| // Compact a couple of events that were more than an hour ago, but less than |
| // two. |
| base::Time first_run = epoch + base::Hours(3); |
| ClickinessCompactionEvents events; |
| events.uncompacted_events.add_timestamps( |
| sql::Statement::TimeToSqlValue(first_run - base::Minutes(65))); |
| events.uncompacted_events.add_timestamps( |
| sql::Statement::TimeToSqlValue(first_run - base::Minutes(70))); |
| std::optional<ClickinessCompactionEvents> events2 = |
| ComputeCompactClickiness(first_run, events); |
| ASSERT_TRUE(events2); |
| EXPECT_EQ(0, events2->uncompacted_events.timestamps_size()); |
| ASSERT_EQ(1, events2->compacted_events.timestamp_and_counts_size()); |
| EXPECT_EQ(2, events2->compacted_events.timestamp_and_counts(0).count()); |
| // These get rounded all the way back to base + 1h. |
| EXPECT_EQ(base::Hours(1).InMicroseconds(), |
| events2->compacted_events.timestamp_and_counts(0).timestamp()); |
| |
| // Now roll back the clock by two hours so they're under one hour old, even |
| // in their post-rounding form. |
| base::Time second_run = epoch + base::Hours(1); |
| std::optional<ClickinessCompactionEvents> events3 = |
| ComputeCompactClickiness(second_run, *events2); |
| |
| // There should be two uncompacted events, with same timestamp. Also this |
| // shouldn't DCHECK-fail. |
| ASSERT_EQ(2, events3->uncompacted_events.timestamps_size()); |
| EXPECT_EQ(base::Hours(1).InMicroseconds(), |
| events3->uncompacted_events.timestamps(0)); |
| EXPECT_EQ(base::Hours(1).InMicroseconds(), |
| events3->uncompacted_events.timestamps(1)); |
| EXPECT_EQ(0, events3->compacted_events.timestamp_and_counts_size()); |
| } |
| |
| // Try to compact a couple of events that have timestamps in the future; they |
| // should be unchanged. |
| TEST_P(InterestGroupStorageDualLifetimeTest, ClickinessCompactTimeFuture) { |
| base::Time epoch = base::Time::FromDeltaSinceWindowsEpoch(base::TimeDelta()); |
| |
| base::Time first_run = epoch + base::Hours(3); |
| ClickinessCompactionEvents events; |
| events.uncompacted_events.add_timestamps( |
| sql::Statement::TimeToSqlValue(first_run + base::Minutes(65))); |
| events.uncompacted_events.add_timestamps( |
| sql::Statement::TimeToSqlValue(first_run + base::Minutes(70))); |
| |
| std::optional<ClickinessCompactionEvents> events2 = |
| ComputeCompactClickiness(first_run, events); |
| ASSERT_TRUE(events2); |
| ASSERT_EQ(2, events2->uncompacted_events.timestamps_size()); |
| EXPECT_EQ(0, events2->compacted_events.timestamp_and_counts_size()); |
| EXPECT_EQ(events.uncompacted_events.timestamps(0), |
| events2->uncompacted_events.timestamps(0)); |
| EXPECT_EQ(events.uncompacted_events.timestamps(1), |
| events2->uncompacted_events.timestamps(1)); |
| } |
| |
| // Test some entries being compacted into an existing category. |
| TEST_P(InterestGroupStorageDualLifetimeTest, ClickinessCompactMerge) { |
| base::Time epoch = base::Time::FromDeltaSinceWindowsEpoch(base::TimeDelta()); |
| |
| base::Time first_run = epoch + base::Hours(3); |
| ClickinessCompactionEvents events; |
| // These get rounded all the way back to base + 1h, which we already have |
| // entries for. |
| events.uncompacted_events.add_timestamps( |
| sql::Statement::TimeToSqlValue(first_run - base::Minutes(65))); |
| events.uncompacted_events.add_timestamps( |
| sql::Statement::TimeToSqlValue(first_run - base::Minutes(70))); |
| auto* entry = events.compacted_events.add_timestamp_and_counts(); |
| entry->set_timestamp(base::Hours(1).InMicroseconds()); |
| entry->set_count(3); |
| |
| std::optional<ClickinessCompactionEvents> events2 = |
| ComputeCompactClickiness(first_run, events); |
| ASSERT_TRUE(events2); |
| EXPECT_EQ(0, events2->uncompacted_events.timestamps_size()); |
| ASSERT_EQ(1, events2->compacted_events.timestamp_and_counts_size()); |
| EXPECT_EQ(base::Hours(1).InMicroseconds(), |
| events2->compacted_events.timestamp_and_counts(0).timestamp()); |
| EXPECT_EQ(5, events2->compacted_events.timestamp_and_counts(0).count()); |
| |
| // Add a few more requests afterwards. |
| events2->uncompacted_events.add_timestamps( |
| sql::Statement::TimeToSqlValue(first_run + base::Minutes(5))); |
| events2->uncompacted_events.add_timestamps( |
| sql::Statement::TimeToSqlValue(first_run + base::Minutes(10))); |
| |
| // Now make everything more than a day old. |
| base::Time second_run = first_run + base::Hours(25); |
| std::optional<ClickinessCompactionEvents> events3 = |
| ComputeCompactClickiness(second_run, *events2); |
| ASSERT_TRUE(events3); |
| EXPECT_EQ(0, events3->uncompacted_events.timestamps_size()); |
| ASSERT_EQ(1, events3->compacted_events.timestamp_and_counts_size()); |
| // Here we get rounded to day base. |
| EXPECT_EQ(base::Hours(0).InMicroseconds(), |
| events3->compacted_events.timestamp_and_counts(0).timestamp()); |
| EXPECT_EQ(7, events3->compacted_events.timestamp_and_counts(0).count()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(DualLifetime, |
| InterestGroupStorageDualLifetimeTest, |
| ::testing::Values(GroupLifetime::k30Day, |
| GroupLifetime::k90Day)); |
| |
| TEST_F(InterestGroupStorageTest, StoresAllFields) { |
| StoresAllFieldsTest(); |
| } |
| |
| #if !BUILDFLAG(IS_IOS) |
| // std::system is not available on iOS. |
| TEST_F(InterestGroupStorageTest, DumpAllIgFields) { |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch("dump-all-ig-fields")) { |
| // This is not part of the proper test, but rather serves as a utility run |
| // on developer workstations for generating new autogenSchemaV[n].sql files |
| // from the current database -- these are used by MultiVersionUpgradeTest. |
| { |
| blink::InterestGroup full = ProduceAllFields(); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| storage->JoinInterestGroup(full, kFullOrigin.GetURL()); |
| } |
| |
| base::FilePath out_sql_path; |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &out_sql_path); |
| out_sql_path = out_sql_path.AppendASCII(base::StringPrintf( |
| "content/test/data/interest_group/autogenSchemaV%d.sql", |
| InterestGroupStorage::GetCurrentVersionNumberForTesting())); |
| // NOTE: This command will be run on POSIX and Windows workstations. To see |
| // command line output for debugging, redirect it to a file. |
| std::string dump_db_command = base::StringPrintf( |
| "sqlite3 %s .dump > %s", db_path().MaybeAsASCII().c_str(), |
| out_sql_path.MaybeAsASCII().c_str()); |
| LOG(INFO) << "--dump-all-ig-fields command (make sure sqlite3 is in $PATH " |
| "/ %PATH%): " |
| << dump_db_command; |
| LOG(INFO) << "sqlite3 can be installed from a package from your OS, or " |
| "built from the Chromium repo via the `sqlite_shell` GN " |
| "target -- just make sure to rename it to / have a symlink " |
| "called sqlite3 on the path."; |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch("dry-run")) { |
| LOG(INFO) << "--dump-all-ig-fields command not run due to --dry-run"; |
| } else { |
| LOG(INFO) << "Running --dump-all-ig-fields command"; |
| EXPECT_EQ(0, std::system(dump_db_command.c_str())); |
| } |
| } |
| } |
| #endif |
| |
| TEST_F(InterestGroupStorageTest, DeleteOriginDeleteAll) { |
| const url::Origin owner_originA = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| const url::Origin owner_originB = |
| url::Origin::Create(GURL("https://owner2.example.com")); |
| const url::Origin owner_originC = |
| url::Origin::Create(GURL("https://owner3.example.com")); |
| const url::Origin joining_originA = |
| url::Origin::Create(GURL("https://joinerA.example.com")); |
| const url::Origin joining_originB = |
| url::Origin::Create(GURL("https://joinerB.example.com")); |
| |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| |
| InterestGroup g1 = NewInterestGroup(owner_originA, "example"); |
| g1.ads.emplace(); |
| g1.ads->push_back(blink::InterestGroup::Ad(ad1_url, "metadata1")); |
| |
| std::string k_anon_key = blink::HashedKAnonKeyForAdBid(g1, ad1_url.spec()); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| storage->JoinInterestGroup(g1, joining_originA.GetURL()); |
| storage->JoinInterestGroup(NewInterestGroup(owner_originB, "example"), |
| joining_originA.GetURL()); |
| storage->JoinInterestGroup(NewInterestGroup(owner_originC, "example"), |
| joining_originA.GetURL()); |
| storage->JoinInterestGroup(NewInterestGroup(owner_originB, "exampleB"), |
| joining_originB.GetURL()); |
| storage->UpdateLastKAnonymityReported(k_anon_key); |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_THAT(origins, UnorderedElementsAre(owner_originA, owner_originB, |
| owner_originC)); |
| std::vector<url::Origin> joining_origins = |
| storage->GetAllInterestGroupJoiningOrigins(); |
| EXPECT_THAT(joining_origins, |
| UnorderedElementsAre(joining_originA, joining_originB)); |
| |
| storage->DeleteInterestGroupData( |
| base::BindLambdaForTesting([&owner_originA]( |
| const blink::StorageKey& storage_key) { |
| return storage_key == |
| blink::StorageKey::CreateFirstParty(owner_originA); |
| }), |
| /*user_initiated_deletion=*/true); |
| |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_THAT(origins, UnorderedElementsAre(owner_originB, owner_originC)); |
| joining_origins = storage->GetAllInterestGroupJoiningOrigins(); |
| EXPECT_THAT(joining_origins, |
| UnorderedElementsAre(joining_originA, joining_originB)); |
| |
| // Delete all interest groups that joined on joining_origin A. We expect that |
| // we will be left with the one that joined on joining_origin B. |
| storage->DeleteInterestGroupData( |
| base::BindLambdaForTesting([&joining_originA]( |
| const blink::StorageKey& storage_key) { |
| return storage_key == |
| blink::StorageKey::CreateFirstParty(joining_originA); |
| }), |
| /*user_initiated_deletion=*/true); |
| |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_THAT(origins, UnorderedElementsAre(owner_originB)); |
| joining_origins = storage->GetAllInterestGroupJoiningOrigins(); |
| EXPECT_THAT(joining_origins, UnorderedElementsAre(joining_originB)); |
| |
| storage->DeleteInterestGroupData(base::NullCallback(), |
| /*user_initiated_deletion=*/false); |
| |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(0u, origins.size()); |
| |
| // DeleteInterestGroupData shouldn't have deleted kanon data. |
| EXPECT_NE(base::Time::Min(), storage->GetLastKAnonymityReported(k_anon_key)); |
| |
| storage->DeleteAllInterestGroupData(); |
| // DeleteAllInterestGroupData should have deleted *everything*. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastKAnonymityReported(k_anon_key)); |
| } |
| |
| TEST_F(InterestGroupStorageTest, DeleteOwnerJoinerPair) { |
| // Set up owner origin, joining origin pairs. |
| const url::Origin owner_originA = |
| url::Origin::Create(GURL("https://ownerA.example.com")); |
| const url::Origin owner_originB = |
| url::Origin::Create(GURL("https://ownerB.example.com")); |
| const url::Origin owner_originC = |
| url::Origin::Create(GURL("https://ownerC.example.com")); |
| const url::Origin joining_originA = |
| url::Origin::Create(GURL("https://joinerA.example.com")); |
| const url::Origin joining_originB = |
| url::Origin::Create(GURL("https://joinerB.example.com")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| storage->JoinInterestGroup(NewInterestGroup(owner_originA, "groupA"), |
| joining_originA.GetURL()); |
| storage->JoinInterestGroup(NewInterestGroup(owner_originB, "groupB"), |
| joining_originA.GetURL()); |
| storage->JoinInterestGroup(NewInterestGroup(owner_originC, "groupC"), |
| joining_originA.GetURL()); |
| storage->JoinInterestGroup(NewInterestGroup(owner_originB, "groupB2"), |
| joining_originB.GetURL()); |
| |
| // Validate that pairs are retrieved correctly. |
| { |
| std::vector<std::pair<url::Origin, url::Origin>> |
| expected_owner_joiner_pairs = {{owner_originA, joining_originA}, |
| {owner_originB, joining_originA}, |
| {owner_originC, joining_originA}, |
| {owner_originB, joining_originB}}; |
| std::vector<std::pair<url::Origin, url::Origin>> owner_joiner_pairs = |
| storage->GetAllInterestGroupOwnerJoinerPairs(); |
| EXPECT_THAT(owner_joiner_pairs, |
| UnorderedElementsAreArray(expected_owner_joiner_pairs)); |
| } |
| |
| // Remove an interest group with specified owner origin and joining origin. |
| storage->RemoveInterestGroupsMatchingOwnerAndJoiner(owner_originB, |
| joining_originA); |
| |
| // Validate that only the specified interest group is removed. |
| { |
| std::vector<std::pair<url::Origin, url::Origin>> |
| expected_owner_joiner_pairs = {{owner_originA, joining_originA}, |
| {owner_originC, joining_originA}, |
| {owner_originB, joining_originB}}; |
| std::vector<std::pair<url::Origin, url::Origin>> owner_joiner_pairs = |
| storage->GetAllInterestGroupOwnerJoinerPairs(); |
| EXPECT_THAT(owner_joiner_pairs, |
| UnorderedElementsAreArray(expected_owner_joiner_pairs)); |
| } |
| } |
| |
| // Maintenance should prune the number of interest groups and interest group |
| // owners based on the set limit. |
| TEST_F(InterestGroupStorageTest, JoinTooManyRegularGroupNames) { |
| base::HistogramTester histograms; |
| const size_t kExcessOwners = 10; |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| const size_t max_groups_per_owner = |
| network::features::kInterestGroupStorageMaxGroupsPerOwner.Get(); |
| const size_t num_groups = max_groups_per_owner + kExcessOwners; |
| std::vector<std::string> added_groups; |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| for (size_t i = 0; i < num_groups; i++) { |
| const std::string group_name = base::NumberToString(i); |
| // Allow time to pass so that they have different expiration times. |
| // This makes which groups get removed deterministic as they are sorted by |
| // expiration time. |
| task_environment().FastForwardBy(base::Microseconds(1)); |
| |
| storage->JoinInterestGroup(NewInterestGroup(test_origin, group_name), |
| test_origin.GetURL()); |
| added_groups.push_back(group_name); |
| } |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(num_groups, interest_groups.size()); |
| histograms.ExpectBucketCount("Storage.InterestGroup.PerSiteCount", num_groups, |
| 1); |
| |
| // Allow enough idle time to trigger maintenance. |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod + |
| base::Seconds(1)); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(max_groups_per_owner, interest_groups.size()); |
| histograms.ExpectBucketCount("Storage.InterestGroup.PerSiteCount", |
| max_groups_per_owner, 1); |
| histograms.ExpectTotalCount("Storage.InterestGroup.PerSiteCount", 2); |
| |
| std::vector<std::string> remaining_groups; |
| for (const auto& db_group : interest_groups) { |
| remaining_groups.push_back(db_group.interest_group.name); |
| } |
| std::vector<std::string> remaining_groups_expected( |
| added_groups.begin() + kExcessOwners, added_groups.end()); |
| EXPECT_THAT(remaining_groups, |
| UnorderedElementsAreArray(remaining_groups_expected)); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBSize", 1); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBMaintenanceTime", 1); |
| } |
| |
| // Maintenance should prune the number of interest groups and interest group |
| // owners based on the set limit. |
| TEST_F(InterestGroupStorageTest, JoinTooManyNegativeGroupNames) { |
| base::HistogramTester histograms; |
| const size_t kExcessOwners = 10; |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| const size_t max_negative_groups_per_owner = |
| network::features::kInterestGroupStorageMaxNegativeGroupsPerOwner.Get(); |
| const size_t num_groups = max_negative_groups_per_owner + kExcessOwners; |
| std::vector<std::string> added_groups; |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| for (size_t i = 0; i < num_groups; i++) { |
| const std::string group_name = base::NumberToString(i); |
| // Allow time to pass so that they have different expiration times. |
| // This makes which groups get removed deterministic as they are sorted by |
| // expiration time. |
| task_environment().FastForwardBy(base::Microseconds(1)); |
| |
| storage->JoinInterestGroup( |
| NewNegativeInterestGroup(test_origin, group_name), |
| test_origin.GetURL()); |
| added_groups.push_back(group_name); |
| } |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| EXPECT_EQ(num_groups, interest_groups.size()); |
| histograms.ExpectBucketCount("Storage.InterestGroup.PerSiteCount", num_groups, |
| 1); |
| |
| // Allow enough idle time to trigger maintenance. |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod + |
| base::Seconds(1)); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(max_negative_groups_per_owner, interest_groups.size()); |
| histograms.ExpectBucketCount("Storage.InterestGroup.PerSiteCount", |
| max_negative_groups_per_owner, 1); |
| histograms.ExpectTotalCount("Storage.InterestGroup.PerSiteCount", 2); |
| |
| std::vector<std::string> remaining_groups; |
| for (const auto& db_group : interest_groups) { |
| remaining_groups.push_back(db_group.interest_group.name); |
| } |
| std::vector<std::string> remaining_groups_expected( |
| added_groups.begin() + kExcessOwners, added_groups.end()); |
| EXPECT_THAT(remaining_groups, |
| UnorderedElementsAreArray(remaining_groups_expected)); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBSize", 1); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBMaintenanceTime", 1); |
| } |
| |
| // Maintenance should prune groups when the interest group owner exceeds the |
| // storage size limit. |
| TEST_F(InterestGroupStorageTest, JoinTooMuchStorage) { |
| base::HistogramTester histograms; |
| const size_t kExcessGroups = 4; |
| const url::Origin kTestOrigin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| const size_t kGroupSize = 800; |
| const size_t groups_before_full = |
| network::features::kInterestGroupStorageMaxStoragePerOwner.Get() / |
| kGroupSize; |
| std::vector<std::string> added_groups; |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| for (size_t i = 0; i < groups_before_full - 1; i++) { |
| const std::string group_name = base::NumberToString(i); |
| // Allow time to pass so that they have different expiration times. |
| // This makes which groups get removed deterministic as they are sorted by |
| // expiration time. |
| task_environment().FastForwardBy(base::Microseconds(1)); |
| blink::InterestGroup group = NewInterestGroup(kTestOrigin, group_name); |
| ASSERT_GT(kGroupSize, group.EstimateSize()); |
| group.user_bidding_signals = |
| std::string(kGroupSize - group.EstimateSize(), 'P'); |
| EXPECT_EQ(kGroupSize, group.EstimateSize()); |
| storage->JoinInterestGroup(group, kTestOrigin.GetURL()); |
| added_groups.push_back(group_name); |
| } |
| |
| const std::string big_group_name = |
| base::NumberToString(groups_before_full - 1); |
| task_environment().FastForwardBy(base::Microseconds(1)); |
| blink::InterestGroup big_group = |
| NewInterestGroup(kTestOrigin, big_group_name); |
| // Let the group be just the size left to reach |
| // `kInterestGroupStorageMaxStoragePerOwner` plus 1, so that this group will |
| // be removed during maintenance. This also guarantees its size is greater |
| // than `kGroupSize`, so that once this group is removed, one more group of |
| // `kGroupSize` can be stored. |
| size_t size_left_before_full = |
| network::features::kInterestGroupStorageMaxStoragePerOwner.Get() - |
| kGroupSize * (groups_before_full - 1); |
| big_group.user_bidding_signals = |
| std::string(size_left_before_full - big_group.EstimateSize() + 1, 'P'); |
| EXPECT_GT(big_group.EstimateSize(), kGroupSize); |
| storage->JoinInterestGroup(big_group, kTestOrigin.GetURL()); |
| added_groups.push_back(big_group_name); |
| |
| for (size_t i = groups_before_full; i < groups_before_full + kExcessGroups; |
| i++) { |
| const std::string group_name = base::NumberToString(i); |
| task_environment().FastForwardBy(base::Microseconds(1)); |
| blink::InterestGroup group = NewInterestGroup(kTestOrigin, group_name); |
| ASSERT_GT(kGroupSize, group.EstimateSize()); |
| group.user_bidding_signals = |
| std::string(kGroupSize - group.EstimateSize(), 'P'); |
| EXPECT_EQ(kGroupSize, group.EstimateSize()); |
| storage->JoinInterestGroup(group, kTestOrigin.GetURL()); |
| added_groups.push_back(group_name); |
| } |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(kTestOrigin); |
| EXPECT_EQ(added_groups.size(), interest_groups.size()); |
| |
| // Allow enough idle time to trigger maintenance. |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod + |
| base::Seconds(1)); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(kTestOrigin); |
| ASSERT_EQ(groups_before_full, interest_groups.size()); |
| |
| std::vector<std::string> remaining_groups; |
| for (const auto& db_group : interest_groups) { |
| remaining_groups.push_back(db_group.interest_group.name); |
| } |
| std::vector<std::string> remaining_groups_expected; |
| // Interest group `groups_before_full` - 1 is removed and one more interest |
| // group `groups_before_full` - 2 can be kept, since the total size is still |
| // within `kInterestGroupStorageMaxStoragePerOwner` with it. |
| for (size_t i = groups_before_full + kExcessGroups - 1; |
| i >= groups_before_full - 2; i--) { |
| if (i != groups_before_full - 1) { |
| remaining_groups_expected.push_back(base::NumberToString(i)); |
| } |
| } |
| EXPECT_THAT(remaining_groups, |
| UnorderedElementsAreArray(remaining_groups_expected)); |
| } |
| |
| // Excess group owners should have their groups pruned by maintenance. |
| // In this test we trigger maintenance by having too many operations in a short |
| // period to test max_ops_before_maintenance_. |
| TEST_F(InterestGroupStorageTest, JoinTooManyGroupOwners) { |
| const size_t kExcessGroups = 10; |
| const size_t max_owners = |
| network::features::kInterestGroupStorageMaxOwners.Get(); |
| const size_t max_ops = |
| network::features::kInterestGroupStorageMaxOpsBeforeMaintenance.Get(); |
| const size_t num_groups = max_owners + kExcessGroups; |
| std::vector<url::Origin> added_origins; |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| for (size_t i = 0; i < num_groups; i++) { |
| const url::Origin test_origin = url::Origin::Create(GURL( |
| base::StrCat({"https://", base::NumberToString(i), ".example.com/"}))); |
| |
| // Allow time to pass so that they have different expiration times. |
| // This makes which groups get removed deterministic as they are sorted by |
| // expiration time. |
| task_environment().FastForwardBy(base::Microseconds(1)); |
| |
| storage->JoinInterestGroup(NewInterestGroup(test_origin, "example"), |
| test_origin.GetURL()); |
| added_origins.push_back(test_origin); |
| } |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_THAT(origins, UnorderedElementsAreArray(added_origins)); |
| |
| // Perform enough operations to trigger maintenance, without passing time. |
| for (size_t i = 0; i < max_ops; i++) { |
| // Any read-only operation will work here. This one should be fast. |
| origins = storage->GetAllInterestGroupOwners(); |
| } |
| |
| // The oldest few interest groups should have been cleared during maintenance. |
| origins = storage->GetAllInterestGroupOwners(); |
| std::vector<url::Origin> remaining_origins_expected( |
| added_origins.begin() + kExcessGroups, added_origins.end()); |
| EXPECT_THAT(origins, UnorderedElementsAreArray(remaining_origins_expected)); |
| } |
| |
| TEST_F(InterestGroupStorageTest, ExpiredGroupsNotReturned) { |
| const char kName1[] = "name1"; |
| const char kName2[] = "name2"; |
| const char kName3[] = "name3"; |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://owner.test")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| const base::TimeDelta kDelta = base::Seconds(1); |
| |
| base::Time start = base::Time::Now(); |
| base::Time later = start + kDelta; |
| base::Time even_later = later + kDelta; |
| |
| // Already expired when joined. |
| storage->JoinInterestGroup( |
| blink::TestInterestGroupBuilder(kOrigin, kName1).SetExpiry(start).Build(), |
| kOrigin.GetURL()); |
| |
| // Expires when time reaches `later`. |
| storage->JoinInterestGroup( |
| blink::TestInterestGroupBuilder(kOrigin, kName2).SetExpiry(later).Build(), |
| kOrigin.GetURL()); |
| |
| // Expires when time reaches `even_later`. |
| storage->JoinInterestGroup(blink::TestInterestGroupBuilder(kOrigin, kName3) |
| .SetExpiry(even_later) |
| .Build(), |
| kOrigin.GetURL()); |
| |
| // All but the first group, which is already expired, should be retrieved. |
| auto interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(2u, interest_groups.size()); |
| EXPECT_THAT( |
| (std::vector<std::string>{interest_groups[0].interest_group.name, |
| interest_groups[1].interest_group.name}), |
| testing::UnorderedElementsAre(kName2, kName3)); |
| |
| // Wait until `later`. The second group should expire. |
| task_environment().FastForwardBy(kDelta); |
| interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| ASSERT_EQ(interest_groups[0].interest_group.name, kName3); |
| |
| // Wait until `even_later`. All three interest groups should now be expired. |
| task_environment().FastForwardBy(kDelta); |
| interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| EXPECT_EQ(0u, interest_groups.size()); |
| } |
| |
| TEST_F(InterestGroupStorageTest, DBMaintenanceExpiresOldInterestGroups) { |
| base::HistogramTester histograms; |
| |
| const url::Origin keep_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| std::vector<::url::Origin> test_origins = { |
| url::Origin::Create(GURL("https://owner.example.com")), |
| url::Origin::Create(GURL("https://owner2.example.com")), |
| url::Origin::Create(GURL("https://owner3.example.com")), |
| }; |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| base::Time original_maintenance_time = |
| storage->GetLastMaintenanceTimeForTesting(); |
| EXPECT_EQ(base::Time::Min(), original_maintenance_time); |
| |
| storage->JoinInterestGroup(NewInterestGroup(keep_origin, "keep"), |
| keep_origin.GetURL()); |
| for (const auto& origin : test_origins) { |
| storage->JoinInterestGroup(NewInterestGroup(origin, "discard"), |
| origin.GetURL()); |
| } |
| |
| std::vector<url::Origin> origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(3u, origins.size()); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(keep_origin); |
| EXPECT_EQ(2u, interest_groups.size()); |
| base::Time next_maintenance_time = |
| base::Time::Now() + InterestGroupStorage::kDefaultIdlePeriod; |
| |
| // Maintenance should not have run yet as we are not idle. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| original_maintenance_time); |
| |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod - |
| base::Seconds(1)); |
| |
| // Maintenance should not have run yet as we are not idle. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| original_maintenance_time); |
| |
| task_environment().FastForwardBy(base::Seconds(2)); |
| // Verify that maintenance has run. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), next_maintenance_time); |
| original_maintenance_time = storage->GetLastMaintenanceTimeForTesting(); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBSize", 1); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBMaintenanceTime", 1); |
| |
| task_environment().FastForwardBy( |
| blink::MaxInterestGroupLifetimeForMetadata() - base::Days(1)); |
| // Verify that maintenance has not run. It's been long enough, but we haven't |
| // made any calls. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| original_maintenance_time); |
| |
| storage->JoinInterestGroup(NewInterestGroup(keep_origin, "keep"), |
| keep_origin.GetURL()); |
| next_maintenance_time = |
| base::Time::Now() + InterestGroupStorage::kDefaultIdlePeriod; |
| |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(3u, origins.size()); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(keep_origin); |
| EXPECT_EQ(2u, interest_groups.size()); |
| |
| // Maintenance should not have run since we have not been idle. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| original_maintenance_time); |
| |
| // Advance past expiration and check that since DB maintenance last ran before |
| // the expiration the outdated entries are present but not reported. |
| task_environment().FastForwardBy(base::Days(1) + base::Seconds(1)); |
| |
| // Verify that maintenance has run. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), next_maintenance_time); |
| original_maintenance_time = storage->GetLastMaintenanceTimeForTesting(); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBSize", 2); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBMaintenanceTime", 2); |
| |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| |
| interest_groups = storage->GetInterestGroupsForOwner(keep_origin); |
| EXPECT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("keep", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| next_maintenance_time = |
| base::Time::Now() + InterestGroupStorage::kDefaultIdlePeriod; |
| |
| // All the groups should still be in the database since they shouldn't have |
| // been cleaned up yet. |
| interest_groups = storage->GetAllInterestGroupsUnfilteredForTesting(); |
| EXPECT_EQ(4u, interest_groups.size()); |
| |
| // Wait an hour to perform DB maintenance. |
| task_environment().FastForwardBy(InterestGroupStorage::kMaintenanceInterval); |
| |
| // Verify that maintenance has run. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), next_maintenance_time); |
| original_maintenance_time = storage->GetLastMaintenanceTimeForTesting(); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBSize", 3); |
| histograms.ExpectTotalCount("Storage.InterestGroup.DBMaintenanceTime", 3); |
| |
| // Verify that the database only contains unexpired entries. |
| origins = storage->GetAllInterestGroupOwners(); |
| EXPECT_EQ(1u, origins.size()); |
| |
| interest_groups = storage->GetAllInterestGroupsUnfilteredForTesting(); |
| EXPECT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("keep", interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| } |
| |
| // Test that when an interest group expires, data about the expired group from |
| // the additional tables (`prev_wins`, `join_count`, `num_bids`) is not |
| // preserved if the interest group is joined again. This tests both the case |
| // where the expired group is destroyed by normal database maintenance, and the |
| // case where it's overwritten by a new group with the same name and owner |
| // before maintenance can be performed. |
| TEST_F(InterestGroupStorageTest, ExpirationDeletesMetadata) { |
| base::HistogramTester histograms; |
| |
| enum class TestCase { |
| // The expired group is destroyed by periodic database maintenance. |
| kDestroyedByMaintenance, |
| // The expired group is overwritten by a new group before database |
| // maintenance has had a chance to destroy it. |
| kOverwrittenByNewGroup |
| }; |
| |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://owner.test")); |
| const char kName[] = "name"; |
| const blink::InterestGroupKey kGroupKey(kOrigin, kName); |
| const char kAdJson[] = "{url: 'https://ad.test/'}"; |
| |
| for (auto test_case : |
| {TestCase::kDestroyedByMaintenance, TestCase::kOverwrittenByNewGroup}) { |
| SCOPED_TRACE(static_cast<int>(test_case)); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| base::Time start = base::Time::Now(); |
| const base::TimeDelta kDelta = base::Seconds(1); |
| |
| // Join the group, and record a bid and win. |
| storage->JoinInterestGroup(blink::TestInterestGroupBuilder(kOrigin, kName) |
| .SetExpiry(start + kDelta) |
| .Build(), |
| kOrigin.GetURL()); |
| storage->RecordInterestGroupBids({kGroupKey}); |
| storage->RecordInterestGroupWin(kGroupKey, kAdJson); |
| |
| // Check that the interest group can be retrieved, and all relevant fields |
| // are correct. |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ(kName, interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->bid_count); |
| ASSERT_EQ(1u, interest_groups[0].bidding_browser_signals->prev_wins.size()); |
| EXPECT_EQ( |
| kAdJson, |
| interest_groups[0].bidding_browser_signals->prev_wins[0]->ad_json); |
| |
| switch (test_case) { |
| case TestCase::kDestroyedByMaintenance: { |
| base::Time expected_maintenance_time = |
| base::Time::Now() + InterestGroupStorage::kDefaultIdlePeriod; |
| // Enough time to trigger maintenance. |
| task_environment().FastForwardBy( |
| InterestGroupStorage::kDefaultIdlePeriod + base::Seconds(1)); |
| // Verify that maintenance has run. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| expected_maintenance_time); |
| break; |
| } |
| |
| case TestCase::kOverwrittenByNewGroup: { |
| base::Time old_maintenance_time = |
| storage->GetLastMaintenanceTimeForTesting(); |
| // Not enough time to trigger maintenance. |
| task_environment().FastForwardBy(base::Seconds(1)); |
| // Maintenance should not have been performed. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| old_maintenance_time); |
| break; |
| } |
| } |
| |
| // Whether or not it's still in the database, GetInterestGroupsForOwner() |
| // should not retrieve the expired group. |
| interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| EXPECT_EQ(0u, interest_groups.size()); |
| |
| // Re-join the interest group. |
| storage->JoinInterestGroup( |
| blink::TestInterestGroupBuilder(kOrigin, kName).Build(), |
| kOrigin.GetURL()); |
| |
| // Retrieve the group. Its `join_count`, `bid_count`, and `prev_wins` should |
| // not reflect data from the first time the group was joined. |
| interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ(kName, interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| EXPECT_EQ(0u, interest_groups[0].bidding_browser_signals->prev_wins.size()); |
| |
| // Leave the interest group so it doesn't affect the next test. |
| storage->LeaveInterestGroup(kGroupKey, kOrigin); |
| } |
| } |
| |
| class InterestGroupStorageWithNoIdleFastForwardTest |
| : public InterestGroupStorageTest { |
| public: |
| // NOTE: For better test runtime performance in FastForwardWithoutIdlingBy(), |
| // use a larger idle period -- this reduces the number of FastForwardBy() |
| // calls. |
| static constexpr base::TimeDelta kIdlePeriod = base::Minutes(30); |
| |
| void SetUp() override { |
| InterestGroupStorageTest::SetUp(); |
| |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| network::features::kInterestGroupStorage, |
| { |
| {"max_ops_before_maintenance", "1000000000"} // 1 billion ops |
| }); |
| |
| GetNetworkService(); |
| // Wait for the Network Service to initialize on the IO thread. |
| RunAllPendingInMessageLoop(content::BrowserThread::IO); |
| // Disable metrics updater to avoid test timeouts when doing long |
| // fast-forwards. |
| network::NetworkService::GetNetworkServiceForTesting() |
| ->ResetMetricsUpdaterForTesting(); |
| } |
| |
| std::unique_ptr<InterestGroupStorage> CreateStorage() { |
| return InterestGroupStorage::CreateWithIdlePeriodForTesting( |
| temp_directory_.GetPath(), /*idle_period=*/kIdlePeriod); |
| } |
| |
| // Fast-forwards time on `task_environment` by `delta` in such a way that |
| // `storage` is never put into an idle state, no matter how large `delta` is. |
| // |
| // This is achieved by breaking the fast-forward up into multiple |
| // fast-forwards, with operations on `storage` in-between to reset the idle |
| // timer. |
| // |
| // Guaranteed to exit with the last `storage` operation occurring at the mock |
| // time of return. |
| static void FastForwardWithoutIdlingBy( |
| base::test::TaskEnvironment& task_environment, |
| InterestGroupStorage& storage, |
| base::TimeDelta delta) { |
| const base::TimeTicks start = base::TimeTicks::Now(); |
| const base::Time last_maintenance_time = |
| storage.GetLastMaintenanceTimeForTesting(); |
| |
| const base::TimeDelta kMaxFastForwardDelta = |
| kIdlePeriod - base::Microseconds(1); |
| for (int64_t i = 0; i < delta.IntDiv(kMaxFastForwardDelta); i++) { |
| SCOPED_TRACE(i); |
| storage.ResetIdleTimerForTesting(); |
| task_environment.FastForwardBy(kMaxFastForwardDelta); |
| EXPECT_EQ(last_maintenance_time, |
| storage.GetLastMaintenanceTimeForTesting()); |
| } |
| storage.ResetIdleTimerForTesting(); |
| task_environment.FastForwardBy(delta % kMaxFastForwardDelta); |
| |
| EXPECT_EQ(last_maintenance_time, |
| storage.GetLastMaintenanceTimeForTesting()); |
| EXPECT_EQ(start + delta, base::TimeTicks::Now()); |
| storage.ResetIdleTimerForTesting(); |
| } |
| |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(InterestGroupStorageWithNoIdleFastForwardTest, ViewClickExpire) { |
| #if BUILDFLAG(IS_MAC) |
| // TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with |
| // unexpected test timeout failures are resolved. |
| if (base::mac::MacOSMajorVersion() == 26) { |
| GTEST_SKIP() << "Disabled on macOS Tahoe."; |
| } |
| #endif |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record_view; |
| record_view.type = AdAuctionEventRecord::Type::kView; |
| record_view.providing_origin = kViewClickProviderOrigin1; |
| record_view.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_view.IsValid()); |
| |
| AdAuctionEventRecord record_click; |
| record_click.type = AdAuctionEventRecord::Type::kClick; |
| record_click.providing_origin = kViewClickProviderOrigin1; |
| record_click.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_click.IsValid()); |
| |
| base::Time start_time = base::Time::Now(); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_click); |
| EXPECT_EQ(true, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin1, kViewClickEligibleOrigin1)); |
| |
| // Quickly fast forward by 91 days (not 90 to account for rounding), |
| // then a bit more to get maintenance to happen with that much time elapsed. |
| FastForwardWithoutIdlingBy(task_environment(), *storage, base::Days(91)); |
| task_environment().FastForwardBy(kIdlePeriod); |
| |
| // Check that maintenance (and therefore compaction) has occurred. |
| EXPECT_LE(start_time + base::Days(91), |
| storage->GetLastMaintenanceTimeForTesting()); |
| |
| EXPECT_EQ(false, |
| storage->CheckViewClickCountsForProviderAndEligibleInDbForTesting( |
| kViewClickProviderOrigin1, kViewClickEligibleOrigin1)); |
| |
| // Doing a fancy high-level read via an IG just gives 0 for all counters. |
| InterestGroup g = NewInterestGroup(kViewClickEligibleOrigin1, "cars"); |
| g.view_and_click_counts_providers = {{kViewClickProviderOrigin1}}; |
| g.expiry = base::Time::Now() + base::Days(90); |
| storage->JoinInterestGroup(g, GURL("https://joining-site.test")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(kViewClickEligibleOrigin1); |
| ASSERT_EQ(1u, groups.size()); |
| |
| blink::mojom::ViewAndClickCountsPtr& view_and_click_counts = |
| groups[0].bidding_browser_signals->view_and_click_counts; |
| |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->view_counts->past_90_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_hour); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_day); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_week); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_30_days); |
| EXPECT_EQ(0, view_and_click_counts->click_counts->past_90_days); |
| } |
| |
| // Regression check for no failures if compaction has to remove multiple rows. |
| TEST_F(InterestGroupStorageWithNoIdleFastForwardTest, ViewClickExpire2) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| AdAuctionEventRecord record_view; |
| record_view.type = AdAuctionEventRecord::Type::kView; |
| record_view.providing_origin = kViewClickProviderOrigin1; |
| record_view.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_view.IsValid()); |
| |
| // View from another provider. |
| AdAuctionEventRecord record_view2; |
| record_view2.type = AdAuctionEventRecord::Type::kView; |
| record_view2.providing_origin = kViewClickProviderOrigin2; |
| record_view2.eligible_origins = {kViewClickEligibleOrigin1}; |
| ASSERT_TRUE(record_view2.IsValid()); |
| |
| base::Time start_time = base::Time::Now(); |
| |
| storage->RecordViewClick(record_view); |
| storage->RecordViewClick(record_view2); |
| |
| // Quickly fast forward by 91 days (not 90 to account for rounding), |
| // then a bit more to get maintenance to happen with that much time elapsed. |
| FastForwardWithoutIdlingBy(task_environment(), *storage, base::Days(91)); |
| task_environment().FastForwardBy(kIdlePeriod); |
| |
| // Check that maintenance (and therefore compaction) has occurred. |
| EXPECT_LE(start_time + base::Days(91), |
| storage->GetLastMaintenanceTimeForTesting()); |
| } |
| |
| // Like InterestGroupStorage.ExpirationDeletesMetadata, but it also checks edge |
| // cases near the expiration point. |
| // |
| // Join and bid history (but not prevWins history) expires at UTC midnight |
| // before the max interest group lifetime expiration -- this is because bid and |
| // join times are only stored at UTC day resolution, to reduce the performance |
| // and storage impact on the database. |
| // |
| // Therefore, for interest groups whose lifetime is near the maximum, they |
| // experience 2 expirations -- one for the join and bid history, and one for the |
| // rest of the interest group. This test checks both expirations, both as |
| // enforced by maintenance, and when re-joining without maintenance. |
| TEST_F(InterestGroupStorageWithNoIdleFastForwardTest, |
| ExpirationDeletesMetadata_LargeLifetimes) { |
| #if BUILDFLAG(IS_MAC) |
| // TODO(crbug.com/434660312): Re-enable on macOS 26 once issues with |
| // unexpected test timeout failures are resolved. |
| if (base::mac::MacOSMajorVersion() == 26) { |
| GTEST_SKIP() << "Disabled on macOS Tahoe."; |
| } |
| #endif |
| |
| base::HistogramTester histograms; |
| |
| // NOTE: These must be large enough for the fast forwards and maintenance |
| // interval used in the test, as checked by an assertion below. We'll also |
| // need to ensure that the test starting time is several hours after midnight |
| // UTC for this to be true, so go with noon tomorrow UTC. |
| const base::TimeDelta kExpiryDeltas[] = { |
| blink::MaxInterestGroupLifetimeForMetadata() - base::Microseconds(1), |
| blink::MaxInterestGroupLifetimeForMetadata()}; |
| |
| const base::Time noon_tomorrow_utc = |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Time::Now().ToDeltaSinceWindowsEpoch().FloorToMultiple( |
| base::Days(1))) + |
| base::Days(1) + base::Hours(12); |
| task_environment().FastForwardBy(noon_tomorrow_utc - base::Time::Now()); |
| |
| enum class TestCase { |
| // The expired group is destroyed by periodic database maintenance, checking |
| // no destruction at expiry - 1 microsecond, and no history loss 1 |
| // microsecond before its expiration (see "History expiration" note below). |
| kDestroyedByMaintenance0, |
| // The expired group is destroyed by periodic database maintenance, checking |
| // destruction at expiry time, and checking history loss at history loss |
| // time (see "History expiration" note below). |
| kDestroyedByMaintenance1, |
| // The expired group is overwritten by a new group before database |
| // maintenance has had a chance to destroy it. Also, check history loss |
| // without running maintenance. |
| kOverwrittenByNewGroup |
| }; |
| |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://owner.test")); |
| const char kName[] = "name"; |
| const blink::InterestGroupKey kGroupKey(kOrigin, kName); |
| const char kAdJson[] = "{url: 'https://ad.test/'}"; |
| |
| for (base::TimeDelta expiry_delta : kExpiryDeltas) { |
| SCOPED_TRACE(expiry_delta); |
| for (auto test_case : {TestCase::kDestroyedByMaintenance0, |
| TestCase::kDestroyedByMaintenance1, |
| TestCase::kOverwrittenByNewGroup}) { |
| SCOPED_TRACE(static_cast<int>(test_case)); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| const base::Time start = base::Time::Now(); |
| const base::Time expiry = start + expiry_delta; |
| // History expiry: Join and bid history expire at a point before the |
| // interest group. This is because these counts are kept on a per UTC day |
| // basis. Win history isn't affected, only join and bid history. |
| const base::Time join_bid_expiry = base::Time::FromDeltaSinceWindowsEpoch( |
| (start + blink::MaxInterestGroupLifetimeForMetadata()) |
| .ToDeltaSinceWindowsEpoch() |
| .FloorToMultiple(base::Days(1))); |
| // Make sure `expiry_delta` is big enough for the required fast forwards |
| // -- we have to wait the idle period for the first maintenance operation, |
| // and we have to at least go kMaintenanceInterval between maintenance |
| // operations. |
| ASSERT_GT(expiry, join_bid_expiry + kIdlePeriod + |
| InterestGroupStorage::kMaintenanceInterval); |
| |
| // Join the group twice (since join counts will always be at least 1, even |
| // after expiration), and record a bid and win. |
| constexpr size_t kJoinCount = 2u; |
| for (size_t i = 0; i < kJoinCount; i++) { |
| storage->JoinInterestGroup( |
| blink::TestInterestGroupBuilder(kOrigin, kName) |
| .SetExpiry(expiry) |
| .Build(), |
| kOrigin.GetURL()); |
| } |
| storage->RecordInterestGroupBids({kGroupKey}); |
| storage->RecordInterestGroupWin(kGroupKey, kAdJson); |
| |
| // Check that the interest group can be retrieved, and all relevant fields |
| // are correct. |
| auto expect_group_original_values = [&storage, &kOrigin, &kName, |
| &kAdJson] { |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ(kName, interest_groups[0].interest_group.name); |
| EXPECT_EQ(2, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->bid_count); |
| ASSERT_EQ(1u, |
| interest_groups[0].bidding_browser_signals->prev_wins.size()); |
| EXPECT_EQ( |
| kAdJson, |
| interest_groups[0].bidding_browser_signals->prev_wins[0]->ad_json); |
| }; |
| expect_group_original_values(); |
| |
| // After passing `join_bid_expiry`, join and bid history will be lost, but |
| // win history retained. |
| auto expect_group_lost_join_bids = [&storage, &kOrigin, &kName, |
| &kAdJson] { |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ(kName, interest_groups[0].interest_group.name); |
| // The join history was lost, resulting in a 0 join count. |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| ASSERT_EQ(1u, |
| interest_groups[0].bidding_browser_signals->prev_wins.size()); |
| EXPECT_EQ( |
| kAdJson, |
| interest_groups[0].bidding_browser_signals->prev_wins[0]->ad_json); |
| }; |
| |
| auto maintenance_test_cases = |
| [&](base::TimeDelta fast_forward_time_before_expire) { |
| // Fast forward not quite enough to expire join / bid history, but |
| // don't trigger maintenance by avoiding idling. |
| FastForwardWithoutIdlingBy( |
| task_environment(), *storage, |
| (join_bid_expiry - fast_forward_time_before_expire) - |
| base::Time::Now()); |
| expect_group_original_values(); |
| |
| // Now, fast forward enough time to trigger maintenance. |
| base::Time expected_maintenance_time = |
| base::Time::Now() + kIdlePeriod; |
| task_environment().FastForwardBy(kIdlePeriod); |
| // Verify that maintenance has run and join / bid history was lost. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| expected_maintenance_time); |
| expect_group_lost_join_bids(); |
| |
| // Now, fast forward not quite enough to expire the interest group, |
| // but don't trigger maintenance by avoiding idling. |
| FastForwardWithoutIdlingBy( |
| task_environment(), *storage, |
| (expiry - fast_forward_time_before_expire) - base::Time::Now()); |
| expect_group_lost_join_bids(); |
| |
| // Now, fast forward enough time to trigger maintenance again. |
| expected_maintenance_time = base::Time::Now() + kIdlePeriod; |
| task_environment().FastForwardBy(kIdlePeriod); |
| // Verify that maintenance has run. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| expected_maintenance_time); |
| }; |
| switch (test_case) { |
| case TestCase::kDestroyedByMaintenance0: { |
| maintenance_test_cases( |
| /*fast_forward_time_before_expire=*/base::Microseconds(1)); |
| break; |
| } |
| |
| case TestCase::kDestroyedByMaintenance1: { |
| maintenance_test_cases( |
| /*fast_forward_time_before_expire=*/kIdlePeriod); |
| break; |
| } |
| |
| case TestCase::kOverwrittenByNewGroup: { |
| base::Time old_maintenance_time = |
| storage->GetLastMaintenanceTimeForTesting(); |
| // Fast forward not quite enough to expire join / bid history, but |
| // don't trigger maintenance by avoiding idling. |
| FastForwardWithoutIdlingBy( |
| task_environment(), *storage, |
| (join_bid_expiry - base::Microseconds(1)) - base::Time::Now()); |
| expect_group_original_values(); |
| |
| task_environment().FastForwardBy(base::Microseconds(2)); |
| expect_group_lost_join_bids(); |
| |
| // Now, fast forward not quite enough to expire the interest group, |
| // but don't trigger maintenance by avoiding idling. |
| FastForwardWithoutIdlingBy( |
| task_environment(), *storage, |
| (expiry - base::Microseconds(1)) - base::Time::Now()); |
| expect_group_lost_join_bids(); |
| |
| task_environment().FastForwardBy(base::Microseconds(1)); |
| |
| // Maintenance should not have been performed. |
| EXPECT_EQ(storage->GetLastMaintenanceTimeForTesting(), |
| old_maintenance_time); |
| break; |
| } |
| } |
| |
| // Whether or not it's still in the database, GetInterestGroupsForOwner() |
| // should not retrieve the expired group. |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(kOrigin); |
| EXPECT_EQ(0u, interest_groups.size()); |
| |
| // Re-join the interest group. |
| storage->JoinInterestGroup( |
| blink::TestInterestGroupBuilder(kOrigin, kName).Build(), |
| kOrigin.GetURL()); |
| |
| // Retrieve the group. Its `join_count`, `bid_count`, and `prev_wins` |
| // should not reflect data from the first time the group was joined. |
| interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ(kName, interest_groups[0].interest_group.name); |
| EXPECT_EQ(1, interest_groups[0].bidding_browser_signals->join_count); |
| EXPECT_EQ(0, interest_groups[0].bidding_browser_signals->bid_count); |
| EXPECT_EQ(0u, |
| interest_groups[0].bidding_browser_signals->prev_wins.size()); |
| |
| // Leave the interest group so it doesn't affect the next test. |
| storage->LeaveInterestGroup(kGroupKey, kOrigin); |
| } |
| } |
| } |
| |
| TEST_F(InterestGroupStorageTest, |
| SelectableBuyerAndSellerReportingIdsDisappearWhenDealSupportDisabled) { |
| const url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| InterestGroup g = NewInterestGroup(test_origin, "name"); |
| g.ads.emplace(); |
| g.ads->emplace_back( |
| ad1_url, "metadata1", |
| /*size_group=*/std::nullopt, |
| /*buyer_reporting_id=*/"brid1", |
| /*buyer_and_seller_reporting_id=*/"shrid1", |
| /*selectable_buyer_and_seller_reporting_ids=*/ |
| std::vector<std::string>{"selectable_id1", "selectable_id2"}); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| storage->JoinInterestGroup(g, test_origin.GetURL()); |
| |
| { |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("name", interest_groups[0].interest_group.name); |
| ASSERT_EQ(1u, interest_groups[0].interest_group.ads->size()); |
| EXPECT_THAT(interest_groups[0] |
| .interest_group.ads.value()[0] |
| .selectable_buyer_and_seller_reporting_ids.value(), |
| testing::ElementsAre("selectable_id1", "selectable_id2")); |
| } |
| |
| { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndDisableFeature( |
| blink::features::kFledgeAuctionDealSupport); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| ASSERT_EQ(1u, interest_groups.size()); |
| EXPECT_EQ("name", interest_groups[0].interest_group.name); |
| ASSERT_EQ(1u, interest_groups[0].interest_group.ads->size()); |
| EXPECT_EQ(interest_groups[0] |
| .interest_group.ads.value()[0] |
| .selectable_buyer_and_seller_reporting_ids, |
| std::nullopt); |
| } |
| } |
| |
| // Upgrades a v6 database dump to an expected current database. |
| // The v6 database dump was extracted from the InterestGroups database in |
| // a browser profile by using `sqlite3 dump <path-to-database>` and then |
| // cleaning up and formatting the output. |
| TEST_F(InterestGroupStorageTest, UpgradeFromV6) { |
| // Create V6 database from dump |
| base::FilePath file_path; |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path); |
| file_path = |
| file_path.AppendASCII("content/test/data/interest_group/schemaV6.sql"); |
| ASSERT_TRUE(base::PathExists(file_path)); |
| ASSERT_TRUE(sql::test::CreateDatabaseFromSQL(db_path(), file_path)); |
| |
| auto expected_interest_group_matcher = testing::UnorderedElementsAre( |
| testing::AllOf( |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("expiry", &InterestGroup::expiry, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13293932603076872))), |
| Field("owner", &InterestGroup::owner, |
| url::Origin::Create(GURL("https://owner.example.com"))), |
| Field("name", &InterestGroup::name, |
| "groupNullUserBiddingSignals"), |
| Field("priority", &InterestGroup::priority, 0.0), |
| Field("enable_bidding_signals_prioritization", |
| &InterestGroup::enable_bidding_signals_prioritization, |
| false), |
| Field("priority_vector", &InterestGroup::priority_vector, |
| std::nullopt), |
| Field("priority_signals_overrides", |
| &InterestGroup::priority_signals_overrides, |
| std::nullopt), |
| Field("seller_capabilities", |
| &InterestGroup::seller_capabilities, std::nullopt), |
| Field("all_sellers_capabilities", |
| &InterestGroup::all_sellers_capabilities, |
| SellerCapabilitiesType()), |
| Field("bidding_url", &InterestGroup::bidding_url, |
| GURL("https://owner.example.com/bidder.js")), |
| Field("bidding_wasm_helper_url", |
| &InterestGroup::bidding_wasm_helper_url, std::nullopt), |
| Field("update_url", &InterestGroup::update_url, |
| GURL("https://owner.example.com/update")), |
| Field("trusted_bidding_signals_url", |
| &InterestGroup::trusted_bidding_signals_url, |
| GURL("https://owner.example.com/signals")), |
| Field( |
| "trusted_bidding_signals_keys", |
| &InterestGroup::trusted_bidding_signals_keys, |
| std::vector<std::string>{"groupNullUserBiddingSignals"}), |
| Field( |
| "trusted_bidding_signals_slot_size_mode", |
| &InterestGroup::trusted_bidding_signals_slot_size_mode, |
| InterestGroup::TrustedBiddingSignalsSlotSizeMode::kNone), |
| Field("max_trusted_bidding_signals_url_length", |
| &InterestGroup::max_trusted_bidding_signals_url_length, |
| 0), |
| Field("trusted_bidding_signals_coordinator", |
| &InterestGroup::trusted_bidding_signals_coordinator, |
| std::nullopt), |
| Field("user_bidding_signals", |
| &InterestGroup::user_bidding_signals, std::nullopt), |
| Field("ads", &InterestGroup::ads, |
| testing::Property( |
| "value()", |
| &std::optional< |
| std::vector<blink::InterestGroup::Ad>>::value, |
| testing::ElementsAre(testing::AllOf( |
| Property("render_url", |
| &InterestGroup::Ad::render_url, |
| GURL("https://ads.example.com/1")), |
| Field("metadata", &InterestGroup::Ad::metadata, |
| "[\"4\",\"5\",null,\"6\"]"))))), |
| Field("ad_components", &InterestGroup::ad_components, |
| std::nullopt), |
| Field("ad_sizes", &InterestGroup::ad_components, |
| std::nullopt), |
| Field("size_groups", &InterestGroup::ad_components, |
| std::nullopt))), |
| Field("bidding_browser_signals", |
| &StorageInterestGroup::bidding_browser_signals, |
| testing::AllOf( |
| Pointee(Field( |
| "join_count", |
| &blink::mojom::BiddingBrowserSignals::join_count, 0)), |
| Pointee(Field( |
| "bid_count", |
| &blink::mojom::BiddingBrowserSignals::bid_count, 0)))), |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::IsEmpty()), |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| url::Origin::Create(GURL("https://publisher.example.com"))), |
| Field("join_time", &StorageInterestGroup::join_time, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13291340603081533))), |
| Field("last_updated", &StorageInterestGroup::last_updated, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13291340603081533)))), |
| testing::AllOf( |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("expiry", &InterestGroup::expiry, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13293932603076872))), |
| Field("owner", &InterestGroup::owner, |
| url::Origin::Create(GURL("https://owner.example.com"))), |
| Field("name", &InterestGroup::name, "group1"), |
| Field("priority", &InterestGroup::priority, 0.0), |
| Field("enable_bidding_signals_prioritization", |
| &InterestGroup::enable_bidding_signals_prioritization, |
| false), |
| Field("priority_vector", &InterestGroup::priority_vector, |
| std::nullopt), |
| Field("priority_signals_overrides", |
| &InterestGroup::priority_signals_overrides, |
| std::nullopt), |
| Field("seller_capabilities", |
| &InterestGroup::seller_capabilities, std::nullopt), |
| Field("all_sellers_capabilities", |
| &InterestGroup::all_sellers_capabilities, |
| SellerCapabilitiesType()), |
| Field("bidding_url", &InterestGroup::bidding_url, |
| GURL("https://owner.example.com/bidder.js")), |
| Field("bidding_wasm_helper_url", |
| &InterestGroup::bidding_wasm_helper_url, std::nullopt), |
| Field("update_url", &InterestGroup::update_url, |
| GURL("https://owner.example.com/update")), |
| Field("trusted_bidding_signals_url", |
| &InterestGroup::trusted_bidding_signals_url, |
| GURL("https://owner.example.com/signals")), |
| Field("trusted_bidding_signals_keys", |
| &InterestGroup::trusted_bidding_signals_keys, |
| std::vector<std::string>{"group1"}), |
| Field( |
| "trusted_bidding_signals_slot_size_mode", |
| &InterestGroup::trusted_bidding_signals_slot_size_mode, |
| InterestGroup::TrustedBiddingSignalsSlotSizeMode::kNone), |
| Field("max_trusted_bidding_signals_url_length", |
| &InterestGroup::max_trusted_bidding_signals_url_length, |
| 0), |
| Field("trusted_bidding_signals_coordinator", |
| &InterestGroup::trusted_bidding_signals_coordinator, |
| std::nullopt), |
| Field("user_bidding_signals", |
| &InterestGroup::user_bidding_signals, |
| "[[\"1\",\"2\"]]"), |
| Field("ads", &InterestGroup::ads, |
| testing::Property( |
| "value()", |
| &std::optional< |
| std::vector<blink::InterestGroup::Ad>>::value, |
| testing::ElementsAre(testing::AllOf( |
| Property("render_url", |
| &InterestGroup::Ad::render_url, |
| GURL("https://ads.example.com/1")), |
| Field("metadata", &InterestGroup::Ad::metadata, |
| "[\"4\",\"5\",null,\"6\"]"))))), |
| Field("ad_components", &InterestGroup::ad_components, |
| std::nullopt), |
| Field("ad_sizes", &InterestGroup::ad_components, |
| std::nullopt), |
| Field("size_groups", &InterestGroup::ad_components, |
| std::nullopt))), |
| Field("bidding_browser_signals", |
| &StorageInterestGroup::bidding_browser_signals, |
| testing::AllOf( |
| Pointee(Field( |
| "join_count", |
| &blink::mojom::BiddingBrowserSignals::join_count, 5)), |
| Pointee(Field( |
| "bid_count", |
| &blink::mojom::BiddingBrowserSignals::bid_count, 4)))), |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::IsEmpty()), |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| url::Origin::Create(GURL("https://publisher.example.com"))), |
| Field("join_time", &StorageInterestGroup::join_time, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13291340603081533))), |
| Field("last_updated", &StorageInterestGroup::last_updated, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13291340603081533)))), |
| testing::AllOf( |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("expiry", &InterestGroup::expiry, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13293932603080090))), |
| Field("owner", &InterestGroup::owner, |
| url::Origin::Create(GURL("https://owner.example.com"))), |
| Field("name", &InterestGroup::name, "group2"), |
| Field("priority", &InterestGroup::priority, 0.0), |
| Field("enable_bidding_signals_prioritization", |
| &InterestGroup::enable_bidding_signals_prioritization, |
| false), |
| Field("priority_vector", &InterestGroup::priority_vector, |
| std::nullopt), |
| Field("priority_signals_overrides", |
| &InterestGroup::priority_signals_overrides, |
| std::nullopt), |
| Field("seller_capabilities", |
| &InterestGroup::seller_capabilities, std::nullopt), |
| Field("all_sellers_capabilities", |
| &InterestGroup::all_sellers_capabilities, |
| SellerCapabilitiesType()), |
| Field("bidding_url", &InterestGroup::bidding_url, |
| GURL("https://owner.example.com/bidder.js")), |
| Field("bidding_wasm_helper_url", |
| &InterestGroup::bidding_wasm_helper_url, std::nullopt), |
| Field("update_url", &InterestGroup::update_url, |
| GURL("https://owner.example.com/update")), |
| Field("trusted_bidding_signals_url", |
| &InterestGroup::trusted_bidding_signals_url, |
| GURL("https://owner.example.com/signals")), |
| Field("trusted_bidding_signals_keys", |
| &InterestGroup::trusted_bidding_signals_keys, |
| std::vector<std::string>{"group2"}), |
| Field( |
| "trusted_bidding_signals_slot_size_mode", |
| &InterestGroup::trusted_bidding_signals_slot_size_mode, |
| InterestGroup::TrustedBiddingSignalsSlotSizeMode::kNone), |
| Field("max_trusted_bidding_signals_url_length", |
| &InterestGroup::max_trusted_bidding_signals_url_length, |
| 0), |
| Field("trusted_bidding_signals_coordinator", |
| &InterestGroup::trusted_bidding_signals_coordinator, |
| std::nullopt), |
| Field("user_bidding_signals", |
| &InterestGroup::user_bidding_signals, |
| "[[\"1\",\"3\"]]"), |
| Field("ads", &InterestGroup::ads, |
| testing::Property( |
| "value()", |
| &std::optional< |
| std::vector<blink::InterestGroup::Ad>>::value, |
| testing::ElementsAre(testing::AllOf( |
| Property("render_url", |
| &InterestGroup::Ad::render_url, |
| GURL("https://ads.example.com/1")), |
| Field("metadata", &InterestGroup::Ad::metadata, |
| "[\"4\",\"5\",null,\"6\"]"))))), |
| Field("ad_components", &InterestGroup::ad_components, |
| std::nullopt), |
| Field("ad_sizes", &InterestGroup::ad_components, |
| std::nullopt), |
| Field("size_groups", &InterestGroup::ad_components, |
| std::nullopt))), |
| Field("bidding_browser_signals", |
| &StorageInterestGroup::bidding_browser_signals, |
| testing::AllOf( |
| Pointee(Field( |
| "join_count", |
| &blink::mojom::BiddingBrowserSignals::join_count, 5)), |
| Pointee(Field( |
| "bid_count", |
| &blink::mojom::BiddingBrowserSignals::bid_count, 3)))), |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::IsEmpty()), |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| url::Origin::Create(GURL("https://publisher.example.com"))), |
| Field("join_time", &StorageInterestGroup::join_time, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13291340603089914))), |
| Field("last_updated", &StorageInterestGroup::last_updated, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13291340603089914)))), |
| testing::AllOf( |
| Field( |
| "interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("expiry", &InterestGroup::expiry, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13293932603052561))), |
| Field("owner", &InterestGroup::owner, |
| url::Origin::Create(GURL("https://owner.example.com"))), |
| Field("name", &InterestGroup::name, "group3"), |
| Field("priority", &InterestGroup::priority, 0.0), |
| Field("enable_bidding_signals_prioritization", |
| &InterestGroup::enable_bidding_signals_prioritization, |
| false), |
| Field("priority_vector", &InterestGroup::priority_vector, |
| std::nullopt), |
| Field("priority_signals_overrides", |
| &InterestGroup::priority_signals_overrides, |
| std::nullopt), |
| Field("seller_capabilities", |
| &InterestGroup::seller_capabilities, std::nullopt), |
| Field("all_sellers_capabilities", |
| &InterestGroup::all_sellers_capabilities, |
| SellerCapabilitiesType()), |
| Field("bidding_url", &InterestGroup::bidding_url, |
| GURL("https://owner.example.com/bidder.js")), |
| Field("bidding_wasm_helper_url", |
| &InterestGroup::bidding_wasm_helper_url, std::nullopt), |
| Field("update_url", &InterestGroup::update_url, |
| GURL("https://owner.example.com/update")), |
| Field("trusted_bidding_signals_url", |
| &InterestGroup::trusted_bidding_signals_url, |
| GURL("https://owner.example.com/signals")), |
| Field("trusted_bidding_signals_keys", |
| &InterestGroup::trusted_bidding_signals_keys, |
| std::vector<std::string>{"group3"}), |
| Field( |
| "trusted_bidding_signals_slot_size_mode", |
| &InterestGroup::trusted_bidding_signals_slot_size_mode, |
| InterestGroup::TrustedBiddingSignalsSlotSizeMode::kNone), |
| Field("max_trusted_bidding_signals_url_length", |
| &InterestGroup::max_trusted_bidding_signals_url_length, |
| 0), |
| Field("trusted_bidding_signals_coordinator", |
| &InterestGroup::trusted_bidding_signals_coordinator, |
| std::nullopt), |
| Field("user_bidding_signals", |
| &InterestGroup::user_bidding_signals, |
| "[[\"3\",\"2\"]]"), |
| Field("ads", &InterestGroup::ads, |
| testing::Property( |
| "value()", |
| &std::optional< |
| std::vector<blink::InterestGroup::Ad>>::value, |
| testing::ElementsAre(testing::AllOf( |
| Property("render_url", |
| &InterestGroup::Ad::render_url, |
| GURL("https://ads.example.com/1")), |
| Field("metadata", &InterestGroup::Ad::metadata, |
| "[\"4\",\"5\",null,\"6\"]"))))), |
| Field("ad_components", &InterestGroup::ad_components, |
| std::nullopt), |
| Field("ad_sizes", &InterestGroup::ad_components, |
| std::nullopt), |
| Field("size_groups", &InterestGroup::ad_components, |
| std::nullopt))), |
| Field("bidding_browser_signals", |
| &StorageInterestGroup::bidding_browser_signals, |
| testing::AllOf( |
| Pointee(Field( |
| "join_count", |
| &blink::mojom::BiddingBrowserSignals::join_count, 4)), |
| Pointee(Field( |
| "bid_count", |
| &blink::mojom::BiddingBrowserSignals::bid_count, 4)))), |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::IsEmpty()), |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| url::Origin::Create(GURL("https://publisher.example.com"))), |
| Field("join_time", &StorageInterestGroup::join_time, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13291340603098283))), |
| Field("last_updated", &StorageInterestGroup::last_updated, |
| base::Time::FromDeltaSinceWindowsEpoch( |
| base::Microseconds(13291340603098283))))); |
| |
| // Upgrade and read. |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| ASSERT_TRUE(storage); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetAllInterestGroupsUnfilteredForTesting(); |
| |
| EXPECT_THAT(interest_groups, expected_interest_group_matcher); |
| } |
| |
| // Make sure the database still works if we open it again. |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetAllInterestGroupsUnfilteredForTesting(); |
| |
| EXPECT_THAT(interest_groups, expected_interest_group_matcher); |
| } |
| } |
| |
| // Upgrades a v6 database dump to an expected current database, then attempts to |
| // insert new rows into the migrated database. |
| // |
| // The v6 database dump was extracted from the InterestGroups database in |
| // a browser profile by using `sqlite3 dump <path-to-database>` and then |
| // cleaning up and formatting the output. |
| TEST_F(InterestGroupStorageTest, UpgradeFromV6ThenAcceptNewData) { |
| // Create V6 database from dump |
| base::FilePath file_path; |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path); |
| file_path = |
| file_path.AppendASCII("content/test/data/interest_group/schemaV6.sql"); |
| ASSERT_TRUE(base::PathExists(file_path)); |
| ASSERT_TRUE(sql::test::CreateDatabaseFromSQL(db_path(), file_path)); |
| |
| // Upgrade. |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| ASSERT_TRUE(storage); |
| |
| // Make sure the database can accept new data (including new fields) correctly |
| // after the migration. |
| StoresAllFieldsTest(); |
| } |
| |
| // Upgrades a v16 database dump to an expected current database. |
| // The v16 database dump was extracted from an updated version of |
| // the v6 data dump, then altered to have new k-anon keys -- |
| // the format of k-anon keys has changed between the two versions |
| // (without migration of one key type to the next) |
| // so this new dump is necessary to test changes to the k-anon |
| // table. |
| TEST_F(InterestGroupStorageTest, UpgradeFromV16) { |
| // Create V16 database from dump |
| base::FilePath file_path; |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path); |
| file_path = |
| file_path.AppendASCII("content/test/data/interest_group/schemaV16.sql"); |
| ASSERT_TRUE(base::PathExists(file_path)); |
| ASSERT_TRUE(sql::test::CreateDatabaseFromSQL(db_path(), file_path)); |
| |
| std::string k_anon_bid = crypto::SHA256HashString( |
| "AdBid\n" |
| "https://owner.example.com/\n" |
| "https://owner.example.com/bidder.js\n" |
| "https://ads.example.com/1"); |
| auto expected_interest_group_matcher = testing::UnorderedElementsAre( |
| testing::AllOf( |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("name", &InterestGroup::name, "group1"))), |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid)), |
| // The oldest update time for any key of this interest group should be |
| // used. The oldest time is Time::Min() because not all keys for this |
| // interest group were in the v16 schema. |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, base::Time::Min())), |
| testing::AllOf( |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("name", &InterestGroup::name, "group2"))), |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid)), |
| // The oldest update time for any key of this interest group should be |
| // used. |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, |
| base::Time::Min() + base::Microseconds(3))), |
| testing::AllOf( |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf(Field("name", &InterestGroup::name, "group3"))), |
| Field("hashed_kanon_keys", &StorageInterestGroup::hashed_kanon_keys, |
| testing::UnorderedElementsAre(k_anon_bid)), |
| // The oldest update time for any key of this interest group should be |
| // used. The oldest time is Time::Min() because not all keys for this |
| // interest group were in the v16 schema. |
| Field("last_k_anon_updated", |
| &StorageInterestGroup::last_k_anon_updated, |
| base::Time::Min()))); |
| |
| // Upgrade and read. |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| ASSERT_TRUE(storage); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetAllInterestGroupsUnfilteredForTesting(); |
| |
| EXPECT_THAT(interest_groups, expected_interest_group_matcher); |
| |
| // In the v16 table, there was a k-anon key that doesn't correspond with an |
| // interest group in the interest group table -- make sure this was migrated |
| // as well. |
| std::string key_without_ig_in_ig_table = crypto::SHA256HashString( |
| "AdBid\nhttps://owner.example2.com/\nhttps://owner.example2.com/" |
| "bidder.js\nhttps://ads.example2.com/1"); |
| std::optional<base::Time> last_reported = |
| storage->GetLastKAnonymityReported(key_without_ig_in_ig_table); |
| EXPECT_EQ(last_reported, base::Time::Min() + base::Microseconds(8)); |
| } |
| |
| // The lockout_debugging_only_report table schema is changed from V31 to V32. |
| TEST_F(InterestGroupStorageTest, UpgradeFromV31) { |
| // Create V31 database from dump |
| base::FilePath file_path; |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path); |
| file_path = |
| file_path.AppendASCII("content/test/data/interest_group/schemaV31.sql"); |
| ASSERT_TRUE(base::PathExists(file_path)); |
| ASSERT_TRUE(sql::test::CreateDatabaseFromSQL(db_path(), file_path)); |
| |
| // Upgrade. |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| ASSERT_TRUE(storage); |
| |
| // Make sure the database can accept new data (including new fields) correctly |
| // after the migration. |
| base::Time now_nearest_next_hour = base::Time::FromDeltaSinceWindowsEpoch( |
| base::Time::Now().ToDeltaSinceWindowsEpoch().CeilToMultiple( |
| base::Hours(1))); |
| storage->RecordDebugReportLockout(now_nearest_next_hour, base::Days(90)); |
| std::optional<DebugReportLockoutAndCooldowns> lockout_and_cooldowns = |
| storage->GetDebugReportLockoutAndAllCooldowns(); |
| ASSERT_TRUE(lockout_and_cooldowns.has_value()); |
| ASSERT_TRUE(lockout_and_cooldowns->lockout.has_value()); |
| EXPECT_EQ(now_nearest_next_hour, |
| lockout_and_cooldowns->lockout->starting_time); |
| EXPECT_EQ(base::Days(90), lockout_and_cooldowns->lockout->duration); |
| } |
| |
| TEST_F(InterestGroupStorageTest, MultiVersionUpgradeTest) { |
| constexpr char kMisssingFileError[] = |
| "You can generate the missing .sql file for the current database " |
| "version by running: \n\n" |
| "out/Default/content_unittests " |
| "--gtest_filter=\"*InterestGroupStorage*Test*DumpAllIgFields\" " |
| "--dump-all-ig-fields\n\n" |
| "after installing sqlite3 from your package manager -- you can also " |
| "build the Chromium `sqlite_shell` GN target and rename / symlink it on " |
| "your path as sqlite3. \n\n" |
| "***Make sure to add the generated file to source control***.\n\n"; |
| for (int i = kOldestAllFieldsVersion; |
| i <= InterestGroupStorage::GetCurrentVersionNumberForTesting() - 1; |
| i++) { |
| SCOPED_TRACE(i); |
| |
| base::FilePath file_path; |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path); |
| file_path = file_path.AppendASCII(base::StringPrintf( |
| "content/test/data/interest_group/autogenSchemaV%d.sql", i)); |
| ASSERT_TRUE(base::PathExists(file_path)) |
| << "Older .sql file " << file_path |
| << " missing -- somehow it wasn't committed when the new " |
| "version was introduced? Anyways, you can use `git reset --hard` to " |
| "go back to a commit with that version to regenerate it. Once at " |
| "that commit: \n\n" |
| << kMisssingFileError; |
| |
| ASSERT_TRUE(sql::test::CreateDatabaseFromSQL(db_path(), file_path)); |
| |
| blink::InterestGroup expected = ProduceAllFields(i); |
| |
| // Upgrade and read. |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| ASSERT_TRUE(storage); |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetAllInterestGroupsUnfilteredForTesting(); |
| |
| ASSERT_EQ(1u, interest_groups.size()); |
| const blink::InterestGroup& actual = interest_groups[0].interest_group; |
| // Don't compare `expiry` as it changes every test run. |
| expected.expiry = actual.expiry; |
| IgExpectEqualsForTesting(actual, expected); |
| } |
| |
| // Make sure the database still works if we open it again. |
| { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetAllInterestGroupsUnfilteredForTesting(); |
| |
| ASSERT_EQ(1u, interest_groups.size()); |
| const blink::InterestGroup& actual = interest_groups[0].interest_group; |
| // Don't compare `expiry` as it changes every test run. |
| expected.expiry = actual.expiry; |
| IgExpectEqualsForTesting(actual, expected); |
| |
| bool version_changed_ig_fields; |
| blink::InterestGroup next_version_expected = |
| ProduceAllFields(i + 1, &version_changed_ig_fields); |
| if (version_changed_ig_fields) { |
| // Make sure IgExpect[Not]EqualsForTesting() gets updated to compare the |
| // newly introduced field(s). |
| next_version_expected.expiry = actual.expiry; |
| IgExpectNotEqualsForTesting(actual, next_version_expected); |
| } |
| } |
| |
| // Make sure the metadata table got upgraded correctly. |
| { |
| sql::Database raw_db(sql::test::kTestTag); |
| EXPECT_TRUE(raw_db.Open(db_path())); |
| sql::MetaTable meta; |
| ASSERT_TRUE(meta.Init(&raw_db, 1, 1)); |
| EXPECT_EQ(InterestGroupStorage::GetCurrentVersionNumberForTesting(), |
| meta.GetVersionNumber()); |
| } |
| |
| // Delete the database in case we loop again, creating the database from |
| // another .sql file. |
| base::DeleteFile(db_path()); |
| } |
| |
| // Make sure the current version .sql dump gets produced when introducing new |
| // versions -- it's up to the author to add it to the CL. |
| base::FilePath file_path; |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path); |
| file_path = file_path.AppendASCII(base::StringPrintf( |
| "content/test/data/interest_group/autogenSchemaV%d.sql", |
| InterestGroupStorage::GetCurrentVersionNumberForTesting())); |
| ASSERT_TRUE(base::PathExists(file_path)) |
| << "Missing " << file_path << " -- " << kMisssingFileError; |
| |
| // Also, make sure that the current version matches ProduceAllFields() -- they |
| // might not match if the storage format changed after the initial DB dump for |
| // the current version (that is, it changed before the CL introducing the new |
| // version landed). |
| { |
| ASSERT_TRUE(sql::test::CreateDatabaseFromSQL(db_path(), file_path)); |
| blink::InterestGroup expected = ProduceAllFields(); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetAllInterestGroupsUnfilteredForTesting(); |
| |
| ASSERT_EQ(1u, interest_groups.size()); |
| const blink::InterestGroup& actual = interest_groups[0].interest_group; |
| // Don't compare `expiry` as it changes every test run. |
| expected.expiry = actual.expiry; |
| IgExpectEqualsForTesting(actual, expected); |
| // Delete the database before the next testcase. |
| base::DeleteFile(db_path()); |
| } |
| } |
| |
| TEST_F(InterestGroupStorageTest, |
| ClusteredGroupsClearedWhenClusterChangesOnJoin) { |
| const url::Origin cluster_origin = |
| url::Origin::Create(GURL("https://cluster.com/")); |
| const url::Origin other_origin = |
| url::Origin::Create(GURL("https://other.com/")); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| ASSERT_TRUE(storage); |
| |
| const std::string clusters[] = { |
| {"cluster0"}, {"cluster1"}, {"cluster2"}, {"cluster3"}}; |
| for (const auto& name : clusters) { |
| blink::InterestGroup group = NewInterestGroup(cluster_origin, name); |
| group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(group, cluster_origin.GetURL()); |
| } |
| storage->JoinInterestGroup(NewInterestGroup(cluster_origin, "separate_same"), |
| cluster_origin.GetURL()); |
| storage->JoinInterestGroup(NewInterestGroup(other_origin, "separate_other"), |
| other_origin.GetURL()); |
| { |
| blink::InterestGroup group = |
| NewInterestGroup(cluster_origin, "clustered_different"); |
| group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(group, other_origin.GetURL()); |
| } |
| { |
| blink::InterestGroup group = |
| NewInterestGroup(other_origin, "clustered_other"); |
| group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(group, other_origin.GetURL()); |
| } |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetAllInterestGroupsUnfilteredForTesting(); |
| EXPECT_EQ(8u, interest_groups.size()); |
| |
| { |
| blink::InterestGroup group = NewInterestGroup(cluster_origin, "cluster0"); |
| group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(group, other_origin.GetURL()); |
| } |
| |
| auto expected_interest_group_matcher = testing::UnorderedElementsAre( |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| cluster_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, cluster_origin), |
| Field("name", &InterestGroup::name, "separate_same"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kCompatibilityMode)))), |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| other_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, other_origin), |
| Field("name", &InterestGroup::name, "separate_other"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kCompatibilityMode)))), |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| other_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, cluster_origin), |
| Field("name", &InterestGroup::name, "clustered_different"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kGroupedByOriginMode)))), |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| other_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, other_origin), |
| Field("name", &InterestGroup::name, "clustered_other"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kGroupedByOriginMode)))), |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| other_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, cluster_origin), |
| Field("name", &InterestGroup::name, "cluster0"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kGroupedByOriginMode))))); |
| interest_groups = storage->GetAllInterestGroupsUnfilteredForTesting(); |
| EXPECT_THAT(interest_groups, expected_interest_group_matcher); |
| } |
| |
| TEST_F(InterestGroupStorageTest, |
| ClusteredGroupsClearedWhenClusterChangesOnLeave) { |
| const url::Origin cluster_origin = |
| url::Origin::Create(GURL("https://cluster.com/")); |
| const url::Origin other_origin = |
| url::Origin::Create(GURL("https://other.com/")); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| ASSERT_TRUE(storage); |
| |
| const std::string clusters[] = { |
| {"cluster0"}, {"cluster1"}, {"cluster2"}, {"cluster3"}}; |
| for (const auto& name : clusters) { |
| blink::InterestGroup group = NewInterestGroup(cluster_origin, name); |
| group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(group, cluster_origin.GetURL()); |
| } |
| storage->JoinInterestGroup(NewInterestGroup(cluster_origin, "separate_same"), |
| cluster_origin.GetURL()); |
| storage->JoinInterestGroup(NewInterestGroup(other_origin, "separate_other"), |
| other_origin.GetURL()); |
| |
| { |
| blink::InterestGroup group = |
| NewInterestGroup(cluster_origin, "clustered_different"); |
| group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(group, other_origin.GetURL()); |
| } |
| |
| { |
| blink::InterestGroup group = |
| NewInterestGroup(other_origin, "clustered_other"); |
| group.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| storage->JoinInterestGroup(group, other_origin.GetURL()); |
| } |
| |
| std::vector<StorageInterestGroup> interest_groups = |
| storage->GetAllInterestGroupsUnfilteredForTesting(); |
| EXPECT_EQ(8u, interest_groups.size()); |
| |
| storage->LeaveInterestGroup( |
| blink::InterestGroupKey(cluster_origin, "cluster0"), other_origin); |
| |
| auto expected_interest_group_matcher = testing::UnorderedElementsAre( |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| cluster_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, cluster_origin), |
| Field("name", &InterestGroup::name, "separate_same"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kCompatibilityMode)))), |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| other_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, other_origin), |
| Field("name", &InterestGroup::name, "separate_other"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kCompatibilityMode)))), |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| other_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, cluster_origin), |
| Field("name", &InterestGroup::name, "clustered_different"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kGroupedByOriginMode)))), |
| testing::AllOf( |
| Field("joining_origin", &StorageInterestGroup::joining_origin, |
| other_origin), |
| Field("interest_group", &StorageInterestGroup::interest_group, |
| testing::AllOf( |
| Field("owner", &InterestGroup::owner, other_origin), |
| Field("name", &InterestGroup::name, "clustered_other"), |
| Field("execution_mode", &InterestGroup::execution_mode, |
| blink::InterestGroup::ExecutionMode:: |
| kGroupedByOriginMode))))); |
| |
| interest_groups = storage->GetAllInterestGroupsUnfilteredForTesting(); |
| EXPECT_THAT(interest_groups, expected_interest_group_matcher); |
| } |
| |
| TEST_F(InterestGroupStorageTest, SetGetLastKAnonReported) { |
| url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| GURL ad2_url = GURL("https://owner.example.com/ad2"); |
| GURL ad3_url = GURL("https://owner.example.com/ad3"); |
| |
| InterestGroup g = NewInterestGroup(test_origin, "name"); |
| g.ads.emplace(); |
| g.ads->push_back(blink::InterestGroup::Ad(ad1_url, "metadata1")); |
| g.ads->push_back(blink::InterestGroup::Ad(ad2_url, "metadata2")); |
| g.ad_components.emplace(); |
| g.ad_components->push_back( |
| blink::InterestGroup::Ad(ad1_url, "component_metadata1")); |
| g.ad_components->push_back( |
| blink::InterestGroup::Ad(ad3_url, "component_metadata3")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| std::string k_anon_key_1 = blink::HashedKAnonKeyForAdBid(g, ad1_url.spec()); |
| std::string k_anon_key_2 = blink::HashedKAnonKeyForAdBid(g, ad2_url.spec()); |
| std::string k_anon_key_3 = blink::HashedKAnonKeyForAdComponentBid(ad3_url); |
| |
| std::optional<base::Time> last_report = |
| storage->GetLastKAnonymityReported(k_anon_key_1); |
| EXPECT_EQ(base::Time::Min(), last_report); // Not in the database. |
| |
| // Setting a last reported time for a key that doesn't correspond with an |
| // interest group should work. |
| base::Time expected_last_report = base::Time::Now(); |
| storage->UpdateLastKAnonymityReported(k_anon_key_3); |
| task_environment().FastForwardBy(base::Seconds(1)); |
| last_report = storage->GetLastKAnonymityReported(k_anon_key_3); |
| EXPECT_EQ(expected_last_report, last_report); |
| |
| storage->JoinInterestGroup(g, GURL("https://owner.example.com/join")); |
| |
| // After joining an interest group for a previously joined key, we should |
| // still get the same time. |
| last_report = storage->GetLastKAnonymityReported(k_anon_key_3); |
| EXPECT_EQ(expected_last_report, last_report); |
| |
| last_report = storage->GetLastKAnonymityReported(k_anon_key_1); |
| EXPECT_EQ(last_report, base::Time::Min()); |
| storage->UpdateLastKAnonymityReported(k_anon_key_1); |
| expected_last_report = base::Time::Now(); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| last_report = storage->GetLastKAnonymityReported(k_anon_key_1); |
| EXPECT_EQ(last_report, expected_last_report); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| last_report = storage->GetLastKAnonymityReported(k_anon_key_2); |
| EXPECT_EQ(last_report, base::Time::Min()); |
| storage->UpdateLastKAnonymityReported(k_anon_key_2); |
| expected_last_report = base::Time::Now(); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| last_report = storage->GetLastKAnonymityReported(k_anon_key_2); |
| EXPECT_EQ(last_report, expected_last_report); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| storage->UpdateLastKAnonymityReported(k_anon_key_3); |
| expected_last_report = base::Time::Now(); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| last_report = storage->GetLastKAnonymityReported(k_anon_key_3); |
| EXPECT_EQ(last_report, expected_last_report); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| std::string group_name_key = blink::HashedKAnonKeyForAdNameReporting( |
| g, g.ads->at(0), |
| /*selected_buyer_and_seller_reporting_id=*/std::nullopt); |
| last_report = storage->GetLastKAnonymityReported(group_name_key); |
| EXPECT_EQ(last_report, base::Time::Min()); |
| storage->UpdateLastKAnonymityReported(group_name_key); |
| expected_last_report = base::Time::Now(); |
| |
| task_environment().FastForwardBy(base::Seconds(1)); |
| |
| last_report = storage->GetLastKAnonymityReported(group_name_key); |
| EXPECT_EQ(last_report, expected_last_report); |
| } |
| |
| TEST_F(InterestGroupStorageTest, UpdatePrioritySignalsOverrides) { |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://example.test")); |
| const char kName[] = "Name"; |
| const blink::InterestGroupKey kInterestGroupKey(kOrigin, kName); |
| |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| // Join a group without any priority signals overrides. |
| InterestGroup original_group = NewInterestGroup(kOrigin, kName); |
| // Set priority vector, so can make sure it's never modified. |
| original_group.priority_vector = {{"key1", 0}}; |
| storage->JoinInterestGroup(original_group, |
| /*main_frame_joining_url=*/kOrigin.GetURL()); |
| std::vector<StorageInterestGroup> storage_interest_groups = |
| storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, storage_interest_groups.size()); |
| IgExpectEqualsForTesting(/*actual=*/storage_interest_groups[0].interest_group, |
| /*expected=*/original_group); |
| |
| // Updating a group that has no overrides should add an overrides maps and set |
| // the corresponding keys. |
| base::flat_map<std::string, auction_worklet::mojom::PrioritySignalsDoublePtr> |
| update1; |
| // Can't put these in an inlined initializer list, since those must use |
| // copyable types. |
| update1.emplace("key1", |
| auction_worklet::mojom::PrioritySignalsDouble::New(0)); |
| update1.emplace("key2", auction_worklet::mojom::PrioritySignalsDoublePtr()); |
| update1.emplace("key3", |
| auction_worklet::mojom::PrioritySignalsDouble::New(-4)); |
| update1.emplace("key4", |
| auction_worklet::mojom::PrioritySignalsDouble::New(5)); |
| storage->UpdateInterestGroupPriorityOverrides(kInterestGroupKey, |
| std::move(update1)); |
| storage_interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, storage_interest_groups.size()); |
| EXPECT_EQ(kOrigin, storage_interest_groups[0].interest_group.owner); |
| EXPECT_EQ(kName, storage_interest_groups[0].interest_group.name); |
| EXPECT_EQ(original_group.priority_vector, |
| storage_interest_groups[0].interest_group.priority_vector); |
| EXPECT_EQ( |
| (base::flat_map<std::string, double>{ |
| {"key1", 0}, {"key3", -4}, {"key4", 5}}), |
| storage_interest_groups[0].interest_group.priority_signals_overrides); |
| |
| // Updating a group that has overrides should modify the existing overrides. |
| base::flat_map<std::string, auction_worklet::mojom::PrioritySignalsDoublePtr> |
| update2; |
| // Can't put these in an inlined initializer list, since those must use |
| // copyable types. |
| update2.emplace("key1", auction_worklet::mojom::PrioritySignalsDoublePtr()); |
| update2.emplace("key2", |
| auction_worklet::mojom::PrioritySignalsDouble::New(6)); |
| update2.emplace("key3", |
| auction_worklet::mojom::PrioritySignalsDouble::New(0)); |
| storage->UpdateInterestGroupPriorityOverrides(kInterestGroupKey, |
| std::move(update2)); |
| storage_interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, storage_interest_groups.size()); |
| EXPECT_EQ(kOrigin, storage_interest_groups[0].interest_group.owner); |
| EXPECT_EQ(kName, storage_interest_groups[0].interest_group.name); |
| EXPECT_EQ(original_group.priority_vector, |
| storage_interest_groups[0].interest_group.priority_vector); |
| EXPECT_EQ( |
| (base::flat_map<std::string, double>{ |
| {"key2", 6}, {"key3", 0}, {"key4", 5}}), |
| storage_interest_groups[0].interest_group.priority_signals_overrides); |
| |
| // Try and set overrides to make an InterestGroup that is too big. Update |
| // should fail, and the InterestGroup should be unmodified. |
| std::vector< |
| std::pair<std::string, auction_worklet::mojom::PrioritySignalsDoublePtr>> |
| overrides_too_big_vector; |
| for (size_t i = 0; i < blink::mojom::kMaxInterestGroupSize / sizeof(double); |
| ++i) { |
| overrides_too_big_vector.emplace_back( |
| base::NumberToString(i), |
| auction_worklet::mojom::PrioritySignalsDouble::New(i)); |
| } |
| storage->UpdateInterestGroupPriorityOverrides( |
| kInterestGroupKey, |
| base::flat_map<std::string, |
| auction_worklet::mojom::PrioritySignalsDoublePtr>( |
| std::move(overrides_too_big_vector))); |
| storage_interest_groups = storage->GetInterestGroupsForOwner(kOrigin); |
| ASSERT_EQ(1u, storage_interest_groups.size()); |
| EXPECT_EQ(kOrigin, storage_interest_groups[0].interest_group.owner); |
| EXPECT_EQ(kName, storage_interest_groups[0].interest_group.name); |
| EXPECT_EQ(original_group.priority_vector, |
| storage_interest_groups[0].interest_group.priority_vector); |
| EXPECT_EQ( |
| (base::flat_map<std::string, double>{ |
| {"key2", 6}, {"key3", 0}, {"key4", 5}}), |
| storage_interest_groups[0].interest_group.priority_signals_overrides); |
| } |
| |
| TEST_F(InterestGroupStorageTest, OnlyDeletesExpiredKAnon) { |
| url::Origin test_origin = |
| url::Origin::Create(GURL("https://owner.example.com")); |
| GURL ad1_url = GURL("https://owner.example.com/ad1"); |
| GURL ad2_url = GURL("https://owner.example.com/ad2"); |
| |
| InterestGroup g = NewInterestGroup(test_origin, "name"); |
| blink::InterestGroupKey interest_group_key(g.owner, g.name); |
| g.ads.emplace(); |
| g.ads->push_back(blink::InterestGroup::Ad(ad1_url, "metadata1")); |
| g.ads->push_back(blink::InterestGroup::Ad(ad2_url, "metadata2")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| std::string k_anon_key_1 = blink::HashedKAnonKeyForAdBid(g, ad1_url.spec()); |
| std::string k_anon_key_2 = blink::HashedKAnonKeyForAdBid(g, ad2_url.spec()); |
| |
| storage->JoinInterestGroup(g, GURL("https://owner.example.com/join")); |
| |
| std::vector<StorageInterestGroup> groups = |
| storage->GetInterestGroupsForOwner(test_origin); |
| |
| storage->UpdateLastKAnonymityReported(k_anon_key_1); |
| storage->UpdateLastKAnonymityReported(k_anon_key_2); |
| |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_2)); |
| |
| task_environment().FastForwardBy( |
| InterestGroupStorage::kAdditionalKAnonStoragePeriod); |
| |
| g.ads->pop_back(); // Erasing an ad from an interest group should not have an |
| // effect on how long we store its last reported time. |
| storage->JoinInterestGroup(g, GURL("https://owner.example.com/join")); |
| |
| // The k-anon values should remain a day after their last-updated time. |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_2)); |
| |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod); |
| |
| EXPECT_EQ(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| EXPECT_EQ(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_2)); |
| |
| // Update the reported time for both keys, with k_anon_key_2 |
| // expiring after k_anon_key_1. |
| storage->UpdateLastKAnonymityReported(k_anon_key_1); |
| task_environment().FastForwardBy(base::Hours(2)); |
| storage->UpdateLastKAnonymityReported(k_anon_key_2); |
| |
| task_environment().FastForwardBy( |
| InterestGroupStorage::kAdditionalKAnonStoragePeriod - base::Hours(2)); |
| |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_2)); |
| |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod); |
| |
| EXPECT_EQ(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_2)); |
| |
| // UpdateLastKAnonymityReported should re-activate a k_anon key. |
| storage->UpdateLastKAnonymityReported(k_anon_key_1); |
| |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_2)); |
| |
| // After the interest group expires, we don't need to keep any k-anonymity |
| // data unless it's been reported <1 day ago. |
| storage->JoinInterestGroup(g, GURL("https://owner.example.com/join")); |
| |
| task_environment().FastForwardBy( |
| blink::MaxInterestGroupLifetimeForMetadata()); |
| storage->UpdateLastKAnonymityReported(k_anon_key_1); |
| EXPECT_EQ(1u, storage->GetAllInterestGroupsUnfilteredForTesting().size()); |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod); |
| EXPECT_EQ(0u, storage->GetAllInterestGroupsUnfilteredForTesting().size()); |
| |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| EXPECT_EQ(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_2)); |
| |
| task_environment().FastForwardBy( |
| InterestGroupStorage::kAdditionalKAnonStoragePeriod); |
| EXPECT_NE(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod); |
| |
| EXPECT_EQ(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_1)); |
| EXPECT_EQ(base::Time::Min(), |
| storage->GetLastKAnonymityReported(k_anon_key_2)); |
| } |
| |
| TEST_F(InterestGroupStorageTest, SetGetBiddingAndAuctionKeys) { |
| const url::Origin origin_a = |
| url::Origin::Create(GURL("https://a.example.com")); |
| const url::Origin origin_b = |
| url::Origin::Create(GURL("https://b.example.com")); |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| // No keys should be returned before any values are set. |
| std::string a_loaded_keys, b_loaded_keys; |
| base::Time a_expiration, b_expiration; |
| std::tie(a_expiration, a_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_a); |
| std::tie(b_expiration, b_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_b); |
| EXPECT_TRUE(a_loaded_keys.empty()); |
| EXPECT_TRUE(b_loaded_keys.empty()); |
| |
| // The set values should be returned. |
| std::string a_keys = "A keys in binary proto"; |
| base::Time expiration = base::Time::Now() + base::Seconds(5); |
| storage->SetBiddingAndAuctionServerKeys(origin_a, a_keys, expiration); |
| std::tie(a_expiration, a_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_a); |
| std::tie(b_expiration, b_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_b); |
| EXPECT_EQ(a_loaded_keys, a_keys); |
| EXPECT_TRUE(b_loaded_keys.empty()); |
| EXPECT_EQ(expiration, a_expiration); |
| |
| // Setting values for a different origin shouldn't affect the previously |
| // set values. |
| std::string b_keys = "B keys in binary proto"; |
| storage->SetBiddingAndAuctionServerKeys(origin_b, b_keys, expiration); |
| std::tie(a_expiration, a_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_a); |
| std::tie(b_expiration, b_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_b); |
| EXPECT_EQ(a_loaded_keys, a_keys); |
| EXPECT_EQ(b_loaded_keys, b_keys); |
| EXPECT_EQ(expiration, a_expiration); |
| EXPECT_EQ(expiration, b_expiration); |
| |
| // Resetting the keys should overwrite the previous keys. |
| a_keys = "New A keys in binary proto"; |
| task_environment().FastForwardBy(base::Seconds(2)); |
| expiration = base::Time::Now() + base::Days(7); |
| storage->SetBiddingAndAuctionServerKeys(origin_a, a_keys, expiration); |
| std::tie(a_expiration, a_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_a); |
| std::tie(b_expiration, b_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_b); |
| EXPECT_EQ(a_loaded_keys, a_keys); |
| EXPECT_EQ(b_loaded_keys, b_keys); |
| EXPECT_EQ(expiration, a_expiration); |
| EXPECT_NE(expiration, b_expiration); |
| |
| // Only get unexpired values. |
| task_environment().FastForwardBy(base::Seconds(3)); |
| std::tie(a_expiration, a_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_a); |
| std::tie(b_expiration, b_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_b); |
| EXPECT_EQ(a_loaded_keys, a_keys); |
| EXPECT_TRUE(b_loaded_keys.empty()); |
| EXPECT_EQ(expiration, a_expiration); |
| |
| // DB maintenance should not delete unexpired values. |
| EXPECT_EQ(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| task_environment().FastForwardBy(InterestGroupStorage::kDefaultIdlePeriod); |
| EXPECT_NE(base::Time::Min(), storage->GetLastMaintenanceTimeForTesting()); |
| std::tie(a_expiration, a_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_a); |
| std::tie(b_expiration, b_loaded_keys) = |
| storage->GetBiddingAndAuctionServerKeys(origin_b); |
| EXPECT_EQ(a_loaded_keys, a_keys); |
| EXPECT_EQ(expiration, a_expiration); |
| EXPECT_TRUE(b_loaded_keys.empty()); |
| } |
| |
| TEST_F(InterestGroupStorageTest, WriteAndLoadCachedKAnonKeys) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| constexpr std::string kKey1 = "key1"; |
| constexpr std::string kKey2 = "key2"; |
| constexpr std::string kKey3 = "key3"; |
| constexpr std::string kKey4 = "key4"; |
| |
| base::test::ScopedFeatureList scoped_feature_to_enforce_limit; |
| scoped_feature_to_enforce_limit.InitAndEnableFeatureWithParameters( |
| features::kFledgeCacheKAnonHashedKeys, |
| {{"CacheKAnonHashedKeysTtl", "1h"}}); |
| |
| base::Time now = base::Time::Now(); |
| |
| // No keys are cached before any are written. |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache( |
| {kKey1, kKey2, kKey3, kKey4}, /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::IsEmpty()); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAre(kKey1, kKey2, kKey3, kKey4)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 0, 1); |
| } |
| |
| // Cached values should be reused. |
| EXPECT_TRUE(storage->WriteHashedKAnonymityKeysToCache( |
| /*positive_hashed_keys=*/{kKey1}, |
| /*negative_hashed_keys=*/{kKey2}, |
| /*fetch_time=*/now)); |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache( |
| {kKey1, kKey2, kKey3, kKey4}, /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::ElementsAre(kKey1)); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAre(kKey3, kKey4)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 50, 1); |
| } |
| |
| // Move forward by 30 minutes (half of the TTL); everything's still valid. |
| now += base::Minutes(30); |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache( |
| {kKey1, kKey2, kKey3, kKey4}, /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::ElementsAre(kKey1)); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAre(kKey3, kKey4)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 50, 1); |
| } |
| |
| // Cached entries can be overwritten. We make key2 k-anon now. (This wouldn't |
| // happen in production, because we wouldn't fetch keys from the k-anonymity |
| // server if they're found in the cache.) We also add key3 to the cache as not |
| // k-anonymous. |
| EXPECT_TRUE(storage->WriteHashedKAnonymityKeysToCache( |
| /*positive_hashed_keys=*/{kKey2}, |
| /*negative_hashed_keys=*/{kKey3}, |
| /*fetch_time=*/now)); |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache( |
| {kKey1, kKey2, kKey3, kKey4}, /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::ElementsAre(kKey1, kKey2)); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAre(kKey4)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 75, 1); |
| } |
| |
| // Move forward by 30 minutes (half of the TTL); everything's still valid |
| // because cache entries are still valid right at the TTL. |
| now += base::Minutes(30); |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache( |
| {kKey1, kKey2, kKey3, kKey4}, /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::ElementsAre(kKey1, kKey2)); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAre(kKey4)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 75, 1); |
| } |
| |
| // One minute later, key1, which was written 61 minutes ago (> than the TTL), |
| // is no longer in the cache. |
| now += base::Minutes(1); |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache( |
| {kKey1, kKey2, kKey3, kKey4}, /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::ElementsAre(kKey2)); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAre(kKey1, kKey4)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 50, 1); |
| } |
| |
| // And finally, 60 minutes after that, all keys are expired. |
| now += base::Hours(1); |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache( |
| {kKey1, kKey2, kKey3, kKey4}, /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::IsEmpty()); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAre(kKey1, kKey2, kKey3, kKey4)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 0, 1); |
| } |
| } |
| |
| // Loads a large number of keys to ensure that the batched lookups work. |
| TEST_F(InterestGroupStorageTest, WriteAndLoadCachedKAnonKeys_BatchedLookups) { |
| std::unique_ptr<InterestGroupStorage> storage = CreateStorage(); |
| |
| std::vector<std::string> all_keys; |
| for (size_t i = 0; i < 234; ++i) { |
| all_keys.push_back(base::NumberToString(i)); |
| } |
| |
| base::test::ScopedFeatureList scoped_feature_to_enforce_limit; |
| scoped_feature_to_enforce_limit.InitAndEnableFeatureWithParameters( |
| features::kFledgeCacheKAnonHashedKeys, |
| {{"CacheKAnonHashedKeysTtl", "1h"}}); |
| |
| base::Time now = base::Time::Now(); |
| |
| // No keys are cached before any are written. |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache(all_keys, |
| /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::IsEmpty()); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAreArray(all_keys)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 0, 1); |
| } |
| |
| std::vector<std::string> positive_keys; |
| std::vector<std::string> negative_keys; |
| std::vector<std::string> other_keys; |
| for (size_t i = 0; i < 234; ++i) { |
| if (i % 4 == 0) { |
| positive_keys.push_back(base::NumberToString(i)); |
| } else if (i % 2 == 0) { |
| negative_keys.push_back(base::NumberToString(i)); |
| } else { |
| other_keys.push_back(base::NumberToString(i)); |
| } |
| } |
| |
| // Cached values should be reused. |
| EXPECT_TRUE(storage->WriteHashedKAnonymityKeysToCache( |
| /*positive_hashed_keys=*/positive_keys, |
| /*negative_hashed_keys=*/negative_keys, |
| /*fetch_time=*/now)); |
| { |
| base::HistogramTester histograms; |
| InterestGroupStorage::KAnonymityCacheResponse cache_repsonse = |
| storage->LoadPositiveHashedKAnonymityKeysFromCache(all_keys, |
| /*check_time=*/now); |
| EXPECT_THAT(cache_repsonse.positive_hashed_keys_from_cache, |
| testing::ElementsAreArray(positive_keys)); |
| EXPECT_THAT(cache_repsonse.ids_to_query_from_server, |
| testing::ElementsAreArray(other_keys)); |
| histograms.ExpectUniqueSample( |
| "Storage.InterestGroup.KAnonymityKeysCacheHitRate", 50, 1); |
| } |
| } |
| |
| } // namespace |
| } // namespace content |