blob: 7bf4d093a33b1e37951bf68e5eb270c79dd5e90c [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/private_aggregation/private_aggregation_budgeter.h"
#include <stdint.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/task/thread_pool.h"
#include "base/task/updateable_sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "content/browser/private_aggregation/private_aggregation_budget_key.h"
#include "content/browser/private_aggregation/private_aggregation_budget_storage.h"
#include "content/browser/private_aggregation/proto/private_aggregation_budgets.pb.h"
#include "content/browser/storage_partition_impl.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/protobuf/src/google/protobuf/repeated_ptr_field.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
using BudgetEntryValidityStatus =
PrivateAggregationBudgeter::BudgetValidityStatus;
using RequestResult = PrivateAggregationBudgeter::RequestResult;
const base::Time kExampleTime = base::Time::FromJavaTime(1652984901234);
class PrivateAggregationBudgeterUnderTest : public PrivateAggregationBudgeter {
public:
PrivateAggregationBudgeterUnderTest(
scoped_refptr<base::UpdateableSequencedTaskRunner> db_task_runner,
bool exclusively_run_in_memory,
const base::FilePath& path_to_db_dir,
base::OnceClosure on_storage_done_initializing)
: PrivateAggregationBudgeter(db_task_runner,
exclusively_run_in_memory,
path_to_db_dir),
on_storage_done_initializing_(std::move(on_storage_done_initializing)) {
}
~PrivateAggregationBudgeterUnderTest() override = default;
PrivateAggregationBudgeter::StorageStatus GetStorageStatus() const {
return storage_status_;
}
// This function adds the value received directly on the storage. As a result,
// invalid data can be added.
void AddBudgetValueAtTimestamp(const PrivateAggregationBudgetKey& budget_key,
int budget_value,
int64_t timestamp) {
if (raw_storage_ == nullptr)
return;
std::string origin_key = budget_key.origin().Serialize();
proto::PrivateAggregationBudgets budgets;
raw_storage_->budgets_data()->TryGetData(origin_key, &budgets);
google::protobuf::RepeatedPtrField<proto::PrivateAggregationBudgetPerHour>*
hourly_budgets =
budget_key.api() == PrivateAggregationBudgetKey::Api::kFledge
? budgets.mutable_fledge_budgets()
: budgets.mutable_shared_storage_budgets();
proto::PrivateAggregationBudgetPerHour* new_budget = hourly_budgets->Add();
new_budget->set_hour_start_timestamp(timestamp);
new_budget->set_budget_used(budget_value);
raw_storage_->budgets_data()->UpdateData(origin_key, budgets);
}
void DeleteAllData() { raw_storage_->budgets_data()->DeleteAllData(); }
private:
void OnStorageDoneInitializing(
std::unique_ptr<PrivateAggregationBudgetStorage> storage) override {
raw_storage_ = storage.get();
PrivateAggregationBudgeter::OnStorageDoneInitializing(std::move(storage));
if (on_storage_done_initializing_)
std::move(on_storage_done_initializing_).Run();
}
base::OnceClosure on_storage_done_initializing_;
// This storage is owned by the base budgeter class so this raw pointer on the
// derived class is destroyed first and won't become dangling.
raw_ptr<PrivateAggregationBudgetStorage> raw_storage_;
};
// TODO(alexmt): Consider moving logic shared with
// PrivateAggregationBudgetStorageTest to a joint test harness.
class PrivateAggregationBudgeterTest : public testing::Test {
public:
PrivateAggregationBudgeterTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
void SetUp() override {
ASSERT_TRUE(temp_directory_.CreateUniqueTempDir());
db_task_runner_ = base::ThreadPool::CreateUpdateableSequencedTaskRunner(
{base::TaskPriority::BEST_EFFORT, base::MayBlock(),
base::TaskShutdownBehavior::BLOCK_SHUTDOWN,
base::ThreadPolicy::MUST_USE_FOREGROUND});
}
void TearDown() override {
// Ensure destruction tasks are run before the test ends. Otherwise,
// erroneous memory leaks may be detected.
DestroyBudgeter();
task_environment_.RunUntilIdle();
}
void CreateBudgeter(bool exclusively_run_in_memory,
base::OnceClosure on_done_initializing) {
budgeter_ = std::make_unique<PrivateAggregationBudgeterUnderTest>(
db_task_runner_, exclusively_run_in_memory, storage_directory(),
std::move(on_done_initializing));
}
void CreateBudgeterAndWait(bool exclusively_run_in_memory = false) {
base::RunLoop run_loop;
CreateBudgeter(exclusively_run_in_memory,
/*on_done_initializing=*/run_loop.QuitClosure());
run_loop.Run();
}
void DestroyBudgeter() { budgeter_.reset(); }
void EnsureDbFlushes() {
// Ensures any pending writes are run.
task_environment_.FastForwardBy(
PrivateAggregationBudgetStorage::kFlushDelay);
task_environment_.RunUntilIdle();
}
PrivateAggregationBudgeter* budgeter() { return budgeter_.get(); }
void AddBudgetValueAtTimestamp(const PrivateAggregationBudgetKey& budget_key,
int value,
int64_t timestamp) {
budgeter_.get()->AddBudgetValueAtTimestamp(budget_key, value, timestamp);
}
void DeleteAllBudgetData() { budgeter_.get()->DeleteAllData(); }
PrivateAggregationBudgetKey CreateBudgetKey() {
return PrivateAggregationBudgetKey::CreateForTesting(
/*origin=*/url::Origin::Create(GURL("https://a.example/")),
/*api_invocation_time=*/kExampleTime,
/*api-*/ PrivateAggregationBudgetKey::Api::kFledge);
}
base::FilePath db_path() const {
return storage_directory().Append(FILE_PATH_LITERAL("PrivateAggregation"));
}
PrivateAggregationBudgeter::StorageStatus GetStorageStatus() const {
return budgeter_->GetStorageStatus();
}
private:
base::FilePath storage_directory() const { return temp_directory_.GetPath(); }
base::ScopedTempDir temp_directory_;
std::unique_ptr<PrivateAggregationBudgeterUnderTest> budgeter_;
scoped_refptr<base::UpdateableSequencedTaskRunner> db_task_runner_;
base::test::TaskEnvironment task_environment_;
};
TEST_F(PrivateAggregationBudgeterTest, BudgeterCreated_DatabaseInitialized) {
bool is_done_initializing = false;
base::RunLoop run_loop;
CreateBudgeter(/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/base::BindLambdaForTesting([&]() {
is_done_initializing = true;
run_loop.Quit();
}));
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
EXPECT_FALSE(is_done_initializing);
run_loop.Run();
EXPECT_TRUE(is_done_initializing);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kOpen);
}
TEST_F(PrivateAggregationBudgeterTest,
DatabaseInitializationFails_StatusIsClosed) {
// The database initialization will fail to open if its directory already
// exists.
base::CreateDirectory(db_path());
base::RunLoop run_loop;
CreateBudgeter(/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/run_loop.QuitClosure());
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
run_loop.Run();
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializationFailed);
}
TEST_F(PrivateAggregationBudgeterTest, InMemory_StillInitializes) {
base::RunLoop run_loop;
CreateBudgeter(/*exclusively_run_in_memory=*/true,
/*on_done_initializing=*/run_loop.QuitClosure());
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
run_loop.Run();
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kOpen);
}
TEST_F(PrivateAggregationBudgeterTest, DatabaseReopened_DataPersisted) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
budgeter()->ConsumeBudget(PrivateAggregationBudgeter::kMaxBudgetPerScope,
example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
// Ensure database has a chance to persist storage.
EnsureDbFlushes();
DestroyBudgeter();
CreateBudgeterAndWait();
base::RunLoop run_loop;
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 2);
}
TEST_F(PrivateAggregationBudgeterTest,
InMemoryDatabaseReopened_DataNotPersisted) {
int num_queries_processed = 0;
CreateBudgeterAndWait(/*exclusively_run_in_memory=*/true);
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
budgeter()->ConsumeBudget(PrivateAggregationBudgeter::kMaxBudgetPerScope,
example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
// Ensure database has a chance to persist storage.
EnsureDbFlushes();
DestroyBudgeter();
CreateBudgeterAndWait(/*exclusively_run_in_memory=*/true);
base::RunLoop run_loop;
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
num_queries_processed++;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 2);
}
TEST_F(PrivateAggregationBudgeterTest, ConsumeBudgetSameKey) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
// Budget can be increased to below max
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
// Budget can be increased to max
budgeter()->ConsumeBudget(
/*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 1),
example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
base::RunLoop run_loop;
// Budget cannot be increased above max
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 3);
}
TEST_F(PrivateAggregationBudgeterTest, ConsumeBudgetDifferentTimeWindows) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
base::Time reference_time = base::Time::FromJavaTime(1652984901234);
// Create a day's worth of budget keys for a particular origin-API pair
// (with varying time windows) plus one extra.
std::vector<PrivateAggregationBudgetKey> example_keys;
for (int i = 0; i < 25; ++i) {
example_keys.push_back(PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example")),
reference_time + i * base::Hours(1),
PrivateAggregationBudgetKey::Api::kFledge));
}
// Consuming this budget 24 times in a day would not exceed the daily budget,
// but 25 times would.
int budget_to_use_per_hour =
PrivateAggregationBudgeter::kMaxBudgetPerScope / 24;
EXPECT_GT(budget_to_use_per_hour * 25,
PrivateAggregationBudgeter::kMaxBudgetPerScope);
// Use budget in the first 24 keys.
for (int i = 0; i < 24; ++i) {
budgeter()->ConsumeBudget(
budget_to_use_per_hour, example_keys[i],
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
}
// The last 24 keys are used for calculating remaining budget, so we can't
// use more during the 24th time window.
budgeter()->ConsumeBudget(budget_to_use_per_hour, example_keys[23],
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result,
RequestResult::kInsufficientBudget);
++num_queries_processed;
}));
base::RunLoop run_loop;
// But the last key can use budget as the first key is no longer in the
// relevant set of 24 time windows.
budgeter()->ConsumeBudget(
budget_to_use_per_hour, example_keys[24],
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 26);
}
TEST_F(PrivateAggregationBudgeterTest, ConsumeBudgetDifferentApis) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey fledge_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
PrivateAggregationBudgetKey shared_storage_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kSharedStorage);
budgeter()->ConsumeBudget(
PrivateAggregationBudgeter::kMaxBudgetPerScope, fledge_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult request) {
EXPECT_EQ(request, RequestResult::kApproved);
++num_queries_processed;
}));
base::RunLoop run_loop;
// The budget for one API does not interfere with the other.
budgeter()->ConsumeBudget(
PrivateAggregationBudgeter::kMaxBudgetPerScope, shared_storage_key,
base::BindLambdaForTesting([&](RequestResult request) {
EXPECT_EQ(request, RequestResult::kApproved);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 2);
}
TEST_F(PrivateAggregationBudgeterTest, ConsumeBudgetDifferentOrigins) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey key_a =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
PrivateAggregationBudgetKey key_b =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://b.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
budgeter()->ConsumeBudget(PrivateAggregationBudgeter::kMaxBudgetPerScope,
key_a,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
base::RunLoop run_loop;
// The budget for one origin does not interfere with the other.
budgeter()->ConsumeBudget(
PrivateAggregationBudgeter::kMaxBudgetPerScope, key_b,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 2);
}
TEST_F(PrivateAggregationBudgeterTest, ConsumeBudgetExtremeValues) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
// Request will be rejected if budget non-positive
budgeter()->ConsumeBudget(
/*budget=*/-1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInvalidRequest);
++num_queries_processed;
}));
budgeter()->ConsumeBudget(
/*budget=*/0, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInvalidRequest);
++num_queries_processed;
}));
base::RunLoop run_loop;
// Request will be rejected if budget exceeds maximum
budgeter()->ConsumeBudget(
/*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope + 1),
example_key, base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kRequestedMoreThanTotalBudget);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 3);
}
TEST_F(PrivateAggregationBudgeterTest, BudgetValidityMetricsRecorded) {
CreateBudgeterAndWait();
PrivateAggregationBudgetKey budget_key = CreateBudgetKey();
constexpr int64_t scope_duration =
PrivateAggregationBudgeter::kBudgetScopeDuration.InMicroseconds();
constexpr int64_t window_duration =
PrivateAggregationBudgetKey::TimeWindow::kDuration.InMicroseconds();
int64_t latest_window_start = budget_key.time_window()
.start_time()
.ToDeltaSinceWindowsEpoch()
.InMicroseconds();
int64_t oldest_window_start =
latest_window_start + window_duration - scope_duration;
int64_t after_latest_window_start = latest_window_start + window_duration;
int64_t before_oldest_window_start = oldest_window_start - window_duration;
constexpr int max_budget = PrivateAggregationBudgeter::kMaxBudgetPerScope;
struct HourlyBudget {
int budget;
int64_t timestamp;
};
const struct {
BudgetEntryValidityStatus expected_status;
std::vector<HourlyBudget> budgets;
} kTestCases[] = {
{BudgetEntryValidityStatus::kValid,
{
{1, oldest_window_start},
{1, latest_window_start},
}},
{BudgetEntryValidityStatus::kValidAndEmpty, {}},
{BudgetEntryValidityStatus::kValidButContainsStaleWindow,
{
{1, before_oldest_window_start},
{1, oldest_window_start},
}},
{BudgetEntryValidityStatus::kContainsTimestampInFuture,
{
{1, latest_window_start},
{3, after_latest_window_start},
}},
{BudgetEntryValidityStatus::kContainsValueExceedingLimit,
{
{1, oldest_window_start},
{max_budget + 1, latest_window_start},
}},
{BudgetEntryValidityStatus::kContainsTimestampNotRoundedToHour,
{
{1, oldest_window_start},
{max_budget, latest_window_start + 1},
}},
{BudgetEntryValidityStatus::kContainsNonPositiveValue,
{
{-1, after_latest_window_start},
{3, oldest_window_start + 1},
{max_budget + 1, latest_window_start},
}},
{BudgetEntryValidityStatus::kSpansMoreThanADay,
{
{5, before_oldest_window_start},
{3, latest_window_start},
}}};
for (const auto& test_case : kTestCases) {
base::HistogramTester histograms;
base::RunLoop run_loop;
for (const auto& budget : test_case.budgets) {
AddBudgetValueAtTimestamp(budget_key, budget.budget, budget.timestamp);
}
budgeter()->ConsumeBudget(
/*budget=*/1, budget_key,
base::BindLambdaForTesting([&](RequestResult result) {
DeleteAllBudgetData();
run_loop.Quit();
}));
histograms.ExpectUniqueSample(
"PrivacySandbox.PrivateAggregation.Budgeter.BudgetValidityStatus",
test_case.expected_status, 1);
run_loop.Run();
}
}
TEST_F(PrivateAggregationBudgeterTest,
ConsumeBudgetBeforeInitialized_QueriesAreQueued) {
base::RunLoop run_loop;
CreateBudgeter(/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/run_loop.QuitClosure());
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
// Queries should be processed in the order they are received.
int num_queries_processed = 0;
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
EXPECT_EQ(++num_queries_processed, 1);
}));
budgeter()->ConsumeBudget(
/*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 1),
example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
EXPECT_EQ(++num_queries_processed, 2);
}));
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
EXPECT_EQ(++num_queries_processed, 3);
}));
EXPECT_EQ(num_queries_processed, 0);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 3);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kOpen);
}
TEST_F(PrivateAggregationBudgeterTest,
ConsumeBudgetBeforeFailedInitialization_QueuedQueriesAreRejected) {
// The database initialization will fail to open if its directory already
// exists.
base::CreateDirectory(db_path());
base::RunLoop run_loop;
CreateBudgeter(/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/run_loop.QuitClosure());
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
// Queries should be processed in the order they are received.
int num_queries_processed = 0;
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kStorageInitializationFailed);
EXPECT_EQ(++num_queries_processed, 1);
}));
budgeter()->ConsumeBudget(
/*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 1),
example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kStorageInitializationFailed);
EXPECT_EQ(++num_queries_processed, 2);
}));
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kStorageInitializationFailed);
EXPECT_EQ(++num_queries_processed, 3);
}));
EXPECT_EQ(num_queries_processed, 0);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 3);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializationFailed);
}
TEST_F(PrivateAggregationBudgeterTest,
MaxPendingCallsExceeded_AdditionalConsumeBudgetCallsRejected) {
base::RunLoop run_loop;
CreateBudgeter(/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/run_loop.QuitClosure());
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
int num_queries_succeeded = 0;
for (int i = 0; i < PrivateAggregationBudgeter::kMaxPendingCalls; ++i) {
// Queries should be processed in the order they are received.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_succeeded, i](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
EXPECT_EQ(num_queries_succeeded++, i);
}));
}
// This query should be immediately rejected.
bool was_callback_run = false;
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kTooManyPendingCalls);
EXPECT_EQ(num_queries_succeeded, 0);
was_callback_run = true;
}));
EXPECT_EQ(num_queries_succeeded, 0);
EXPECT_TRUE(was_callback_run);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
run_loop.Run();
EXPECT_EQ(num_queries_succeeded,
PrivateAggregationBudgeter::kMaxPendingCalls);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kOpen);
}
TEST_F(PrivateAggregationBudgeterTest,
MaxPendingCallsExceeded_AdditionalDataClearingCallsAllowed) {
base::RunLoop run_loop;
CreateBudgeter(/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/base::DoNothing());
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
base::Time::FromJavaTime(1652984901234),
PrivateAggregationBudgetKey::Api::kFledge);
int num_consume_queries_succeeded = 0;
for (int i = 0; i < PrivateAggregationBudgeter::kMaxPendingCalls; ++i) {
// Queries should be processed in the order they are received.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_consume_queries_succeeded, i](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
EXPECT_EQ(num_consume_queries_succeeded++, i);
}));
}
EXPECT_EQ(num_consume_queries_succeeded, 0);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
// Despite the limit being reached, data clearing requests are allowed to
// cause the limit to be exceeded and are queued.
bool was_callback_run = false;
budgeter()->ClearData(base::Time::Min(), base::Time::Max(),
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
was_callback_run = true;
run_loop.Quit();
}));
EXPECT_FALSE(was_callback_run);
run_loop.Run();
EXPECT_EQ(num_consume_queries_succeeded,
PrivateAggregationBudgeter::kMaxPendingCalls);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kOpen);
EXPECT_TRUE(was_callback_run);
}
TEST_F(PrivateAggregationBudgeterTest,
BudgeterDestroyedImmediatelyAfterInitialization_DoesNotCrash) {
base::RunLoop run_loop;
CreateBudgeter(
/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/base::BindLambdaForTesting([this, &run_loop]() {
DestroyBudgeter();
run_loop.Quit();
}));
run_loop.Run();
}
TEST_F(PrivateAggregationBudgeterTest,
BudgeterDestroyedImmediatelyAfterCreation_DoesNotCrash) {
CreateBudgeter(/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/base::DoNothing());
DestroyBudgeter();
base::RunLoop().RunUntilIdle();
}
TEST_F(PrivateAggregationBudgeterTest, ClearDataBasicTest) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kFledge);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
}));
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
budgeter()->ClearData(kExampleTime, kExampleTime,
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
}
TEST_F(PrivateAggregationBudgeterTest, ClearDataCrossesWindowBoundary) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key_1 =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kFledge);
PrivateAggregationBudgetKey example_key_2 =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
kExampleTime + PrivateAggregationBudgetKey::TimeWindow::kDuration,
PrivateAggregationBudgetKey::Api::kFledge);
EXPECT_NE(example_key_1.time_window().start_time(),
example_key_2.time_window().start_time());
budgeter()->ConsumeBudget(
/*budget=*/1, example_key_1,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
budgeter()->ConsumeBudget(
/*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 1),
example_key_2,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
// The full budget has been used across the two time windows.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key_2,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
}));
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
budgeter()->ClearData(
kExampleTime,
kExampleTime + PrivateAggregationBudgetKey::TimeWindow::kDuration,
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again.
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_2,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 5);
}
TEST_F(PrivateAggregationBudgeterTest,
ClearDataDoesntAffectWindowsOutsideRange) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey key_to_clear =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kFledge);
PrivateAggregationBudgetKey key_after =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
kExampleTime + PrivateAggregationBudgetKey::TimeWindow::kDuration,
PrivateAggregationBudgetKey::Api::kFledge);
PrivateAggregationBudgetKey key_before =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
kExampleTime - PrivateAggregationBudgetKey::TimeWindow::kDuration,
PrivateAggregationBudgetKey::Api::kFledge);
EXPECT_LT(key_to_clear.time_window().start_time(),
key_after.time_window().start_time());
EXPECT_GT(key_to_clear.time_window().start_time(),
key_before.time_window().start_time());
base::RepeatingCallback<void(RequestResult)> expect_approved =
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
});
budgeter()->ConsumeBudget(
/*budget=*/1, key_before, expect_approved);
budgeter()->ConsumeBudget(
/*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 2),
key_to_clear, expect_approved);
budgeter()->ConsumeBudget(
/*budget=*/1, key_after, expect_approved);
// The full budget has been used across the three time windows.
budgeter()->ConsumeBudget(
/*budget=*/1, key_after,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
}));
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
// This will only clear the `key_to_clear`'s budget.
budgeter()->ClearData(kExampleTime, kExampleTime,
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can have a budget of exactly
// (`PrivateAggregationBudgeter::kMaxBudgetPerScope` - 2) that we can use.
budgeter()->ConsumeBudget(
/*budget=*/(PrivateAggregationBudgeter::kMaxBudgetPerScope - 2),
key_after, expect_approved);
budgeter()->ConsumeBudget(
/*budget=*/1, key_after,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 7);
}
TEST_F(PrivateAggregationBudgeterTest, ClearDataAllApisAffected) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey fledge_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kFledge);
PrivateAggregationBudgetKey shared_storage_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kSharedStorage);
base::RepeatingCallback<void(RequestResult)> expect_approved =
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
});
base::RepeatingCallback<void(RequestResult)> expect_insufficient_budget =
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
});
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, fledge_key,
expect_approved);
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, fledge_key, expect_insufficient_budget);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope,
shared_storage_key, expect_approved);
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, shared_storage_key, expect_insufficient_budget);
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
budgeter()->ClearData(kExampleTime, kExampleTime,
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, fledge_key,
expect_approved);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope,
shared_storage_key, base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 7);
}
TEST_F(PrivateAggregationBudgeterTest, ClearAllDataBasicTest) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kFledge);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
}));
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
budgeter()->ClearData(base::Time::Min(), base::Time::Max(),
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
}
TEST_F(PrivateAggregationBudgeterTest, ClearAllDataNullTimes) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kFledge);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
}));
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
budgeter()->ClearData(base::Time(), base::Time(),
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
}
TEST_F(PrivateAggregationBudgeterTest, ClearAllDataNullStartNonNullEndTime) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kFledge);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
}));
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
budgeter()->ClearData(base::Time(), base::Time::Max(),
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
}
TEST_F(PrivateAggregationBudgeterTest, ClearDataFilterSelectsOrigins) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
const url::Origin kOriginA = url::Origin::Create(GURL("https://a.example/"));
const url::Origin kOriginB = url::Origin::Create(GURL("https://b.example/"));
PrivateAggregationBudgetKey example_key_a =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginA, kExampleTime, PrivateAggregationBudgetKey::Api::kFledge);
PrivateAggregationBudgetKey example_key_b =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginB, kExampleTime, PrivateAggregationBudgetKey::Api::kFledge);
base::RepeatingCallback<void(RequestResult)> expect_approved =
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
});
base::RepeatingCallback<void(RequestResult)> expect_insufficient_budget =
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
});
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_a,
expect_approved);
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key_a, expect_insufficient_budget);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_b,
expect_approved);
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key_b, expect_insufficient_budget);
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
budgeter()->ClearData(
kExampleTime, kExampleTime,
base::BindLambdaForTesting([&](const blink::StorageKey& storage_key) {
return storage_key == blink::StorageKey(kOriginA);
}),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again for the cleared origin.
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_a,
expect_approved);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_b,
expect_insufficient_budget);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 7);
}
TEST_F(PrivateAggregationBudgeterTest, ClearDataAllTimeFilterSelectsOrigins) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
const url::Origin kOriginA = url::Origin::Create(GURL("https://a.example/"));
const url::Origin kOriginB = url::Origin::Create(GURL("https://b.example/"));
PrivateAggregationBudgetKey example_key_a =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginA, kExampleTime, PrivateAggregationBudgetKey::Api::kFledge);
PrivateAggregationBudgetKey example_key_b =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginB, kExampleTime, PrivateAggregationBudgetKey::Api::kFledge);
base::RepeatingCallback<void(RequestResult)> expect_approved =
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
});
base::RepeatingCallback<void(RequestResult)> expect_insufficient_budget =
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientBudget);
++num_queries_processed;
});
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_a,
expect_approved);
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key_a, expect_insufficient_budget);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_b,
expect_approved);
// Maximum budget has been used so this should fail.
budgeter()->ConsumeBudget(
/*budget=*/1, example_key_b, expect_insufficient_budget);
// `ClearData()` runs its callback after a round trip in the db task runner,
// so its callback is invoked last.
base::RunLoop run_loop;
budgeter()->ClearData(
base::Time::Min(), base::Time::Max(),
base::BindLambdaForTesting([&](const blink::StorageKey& storage_key) {
return storage_key == blink::StorageKey(kOriginA);
}),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again for the cleared origin.
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_a,
expect_approved);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key_b,
expect_insufficient_budget);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 7);
}
TEST_F(PrivateAggregationBudgeterTest,
BudgeterDestroyedImmedatelyAfterClearData_CallbackStillRun) {
int num_queries_processed = 0;
CreateBudgeterAndWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationBudgetKey::Api::kFledge);
budgeter()->ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kMaxBudgetPerScope, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
base::RunLoop run_loop;
budgeter()->ClearData(base::Time(), base::Time(),
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
DestroyBudgeter();
// Callback still run even though the budgeter was immediately destroyed.
run_loop.Run();
EXPECT_EQ(num_queries_processed, 2);
}
} // namespace
} // namespace content