blob: 9efcfd6a8aff5fc744706b8d474a9780c1bb0176 [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 <limits>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/barrier_closure.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/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.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/scoped_feature_list.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/private_aggregation_caller_api.h"
#include "content/browser/private_aggregation/proto/private_aggregation_budgets.pb.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/private_aggregation_data_model.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/schemeful_site.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h"
#include "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;
using ResultForContribution = PrivateAggregationBudgeter::ResultForContribution;
using InspectBudgetCallResult =
PrivateAggregationBudgeter::InspectBudgetCallResult;
using BudgetQueryResult = PrivateAggregationBudgeter::BudgetQueryResult;
constexpr auto kExampleTime =
base::Time::FromMillisecondsSinceUnixEpoch(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 site_key = net::SchemefulSite(budget_key.origin()).Serialize();
proto::PrivateAggregationBudgets budgets;
raw_storage_->budgets_data()->TryGetData(site_key, &budgets);
google::protobuf::RepeatedPtrField<proto::PrivateAggregationBudgetEntry>*
budget_entries = budget_key.caller_api() ==
PrivateAggregationCallerApi::kProtectedAudience
? budgets.mutable_protected_audience_budgets()
: budgets.mutable_shared_storage_budgets();
proto::PrivateAggregationBudgetEntry* new_budget = budget_entries->Add();
new_budget->set_entry_start_timestamp(timestamp);
new_budget->set_budget_used(budget_value);
raw_storage_->budgets_data()->UpdateData(site_key, budgets);
}
int NumberOfSitesInStorage() {
if (raw_storage_ == nullptr) {
return 0;
}
return raw_storage_->budgets_data()->GetAllCached().size();
}
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 PrivateAggregationBudgeterTestBase : public testing::Test {
public:
explicit PrivateAggregationBudgeterTestBase(
bool enable_error_reporting_feature)
: enable_error_reporting_feature_(enable_error_reporting_feature),
task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
scoped_feature_list_.InitWithFeatureState(
blink::features::kPrivateAggregationApiErrorReporting,
enable_error_reporting_feature_);
}
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 CreateBudgeterWithoutInitializing(
bool exclusively_run_in_memory = false,
base::OnceClosure on_done_initializing = base::DoNothing()) {
budgeter_ = std::make_unique<PrivateAggregationBudgeterUnderTest>(
db_task_runner_, exclusively_run_in_memory, storage_directory(),
std::move(on_done_initializing));
}
void CreateAndInitializeBudgeterThenWait(
bool exclusively_run_in_memory = false) {
base::RunLoop run_loop;
CreateBudgeterWithoutInitializing(exclusively_run_in_memory);
InitializeBudgeter(/*on_done_initializing=*/run_loop.QuitClosure());
run_loop.Run();
}
// Initializes budgeter's storage by calling `ClearData` (which initiates
// storage initialization) on an empty range.
void InitializeBudgeter(base::OnceClosure on_done_initializing) {
ASSERT_EQ(
GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kPendingInitialization);
budgeter()->ClearData(base::Time::Min(), base::Time::Min(),
StoragePartition::StorageKeyMatcherFunction(),
std::move(on_done_initializing));
ASSERT_GT(
GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kPendingInitialization);
}
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);
}
int NumberOfSitesInStorage() {
return budgeter_.get()->NumberOfSitesInStorage();
}
void DeleteAllBudgetData() { budgeter_.get()->DeleteAllData(); }
PrivateAggregationBudgetKey CreateBudgetKey() {
return PrivateAggregationBudgetKey::CreateForTesting(
/*origin=*/url::Origin::Create(GURL("https://a.example/")),
/*api_invocation_time=*/kExampleTime,
/*caller_api=*/PrivateAggregationCallerApi::kProtectedAudience);
}
base::FilePath db_path() const {
return storage_directory().Append(FILE_PATH_LITERAL("PrivateAggregation"));
}
PrivateAggregationBudgeter::StorageStatus GetStorageStatus() const {
return budgeter_->GetStorageStatus();
}
// Helper to (conditionally) adapt old tests to new error reporting flow.
void ConsumeBudget(int budget,
const PrivateAggregationBudgetKey& budget_key,
base::OnceCallback<void(RequestResult)> on_done) {
if (!enable_error_reporting_feature_) {
budgeter()->ConsumeBudget(budget, budget_key, std::move(on_done));
return;
}
// Bit of a hack to adapt old tests to the new error reporting flow.
// TODO(crbug.com/381788013): Remove hack when feature flag is removed
// (after full launch).
std::vector<blink::mojom::AggregatableReportHistogramContribution>
fake_contribution_vector = {
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/0, /*value=*/int32_t{budget},
/*filtering_id=*/0)};
base::OnceCallback<void(InspectBudgetCallResult)> on_test_result =
base::BindLambdaForTesting([=, this, on_done = std::move(on_done)](
InspectBudgetCallResult result) mutable {
if (!result.lock.has_value()) {
// Handle fatal error
std::move(on_done).Run(
std::move(result).query_result.overall_result);
return;
}
base::OnceCallback<RequestResult(BudgetQueryResult)> adapt_result =
base::BindOnce([](BudgetQueryResult result) {
return result.overall_result;
});
base::OnceCallback<void(BudgetQueryResult)> consume_budget_callback =
std::move(adapt_result).Then(std::move(on_done));
budgeter()->ConsumeBudget(std::move(result.lock.value()),
fake_contribution_vector, budget_key,
std::move(consume_budget_callback));
});
budgeter()->InspectBudgetAndLock(fake_contribution_vector, budget_key,
std::move(on_test_result));
}
protected:
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::ScopedFeatureList scoped_feature_list_;
bool enable_error_reporting_feature_;
base::test::TaskEnvironment task_environment_;
};
class PrivateAggregationBudgeterTest
: public PrivateAggregationBudgeterTestBase,
public testing::WithParamInterface<bool> {
public:
PrivateAggregationBudgeterTest()
: PrivateAggregationBudgeterTestBase(GetErrorReportingEnabledParam()) {}
bool GetErrorReportingEnabledParam() const { return GetParam(); }
};
INSTANTIATE_TEST_SUITE_P(,
PrivateAggregationBudgeterTest,
testing::Bool(),
[](auto& info) {
return info.param ? "ErrorReportingEnabled"
: "ErrorReportingDisabled";
});
class PrivateAggregationBudgeterErrorReportingEnabledTest
: public PrivateAggregationBudgeterTestBase {
public:
PrivateAggregationBudgeterErrorReportingEnabledTest()
: PrivateAggregationBudgeterTestBase(
/*enable_error_reporting_feature=*/true) {}
};
class PrivateAggregationBudgeterErrorReportingDisabledTest
: public PrivateAggregationBudgeterTestBase {
public:
PrivateAggregationBudgeterErrorReportingDisabledTest()
: PrivateAggregationBudgeterTestBase(
/*enable_error_reporting_feature=*/false) {}
};
TEST_P(PrivateAggregationBudgeterTest,
BudgeterCreated_DatabaseInitializedLazily) {
bool is_done_initializing = false;
CreateBudgeterWithoutInitializing(
/*exclusively_run_in_memory=*/false,
/*on_done_initializing=*/base::BindLambdaForTesting(
[&is_done_initializing]() { is_done_initializing = true; }));
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kPendingInitialization);
EXPECT_FALSE(is_done_initializing);
base::RunLoop run_loop;
InitializeBudgeter(/*on_done_initializing=*/run_loop.QuitClosure());
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
run_loop.Run();
EXPECT_TRUE(is_done_initializing);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kOpen);
}
TEST_P(PrivateAggregationBudgeterTest,
DatabaseInitializationFails_StatusIsClosed) {
// The database initialization will fail to open if its directory already
// exists.
base::CreateDirectory(db_path());
CreateAndInitializeBudgeterThenWait();
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializationFailed);
}
TEST_P(PrivateAggregationBudgeterTest, InMemory_StillInitializes) {
CreateAndInitializeBudgeterThenWait(/*exclusively_run_in_memory=*/true);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kOpen);
}
TEST_P(PrivateAggregationBudgeterTest, DatabaseReopened_DataPersisted) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope,
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();
CreateAndInitializeBudgeterThenWait();
base::RunLoop run_loop;
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 2);
}
TEST_P(PrivateAggregationBudgeterTest,
InMemoryDatabaseReopened_DataNotPersisted) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait(/*exclusively_run_in_memory=*/true);
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope,
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();
CreateAndInitializeBudgeterThenWait(/*exclusively_run_in_memory=*/true);
base::RunLoop run_loop;
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_P(PrivateAggregationBudgeterTest, ConsumeBudgetSameKey) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
// Budget can be increased to below max
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
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
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
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 3);
}
TEST_P(PrivateAggregationBudgeterTest,
ConsumeBudgetDifferentWindowsSmallerScope) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
base::Time reference_time = kExampleTime;
// Create 10 min worth of budget keys for a particular site-API pair
// (with varying time windows) plus one extra.
std::vector<PrivateAggregationBudgetKey> example_keys;
for (int i = 0; i < 11; ++i) {
example_keys.push_back(PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example")),
reference_time + i * base::Minutes(1),
PrivateAggregationCallerApi::kProtectedAudience));
}
// Consuming this budget 10 times in 10 min would not exceed the daily budget,
// but 11 times would.
int budget_to_use_per_minute =
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope / 10;
EXPECT_GT(
budget_to_use_per_minute * 11,
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope);
// Use budget in the first 10 keys.
for (int i = 0; i < 10; ++i) {
ConsumeBudget(budget_to_use_per_minute, example_keys[i],
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
}
// The last 10 keys are used for calculating remaining budget, so we can't
// use more during the 10th time window.
ConsumeBudget(budget_to_use_per_minute, example_keys[9],
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result,
RequestResult::kInsufficientSmallerScopeBudget);
++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 10 smaller scope time windows.
ConsumeBudget(budget_to_use_per_minute, example_keys[10],
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 12);
}
TEST_P(PrivateAggregationBudgeterTest,
ConsumeBudgetDifferentWindowsLargerScope) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
base::Time reference_time = kExampleTime;
// Create a day's worth of budget keys for a particular site-API pair
// (with varying time windows) plus one extra.
std::vector<PrivateAggregationBudgetKey> example_keys;
for (int i = 0; i < 1441; ++i) {
example_keys.push_back(PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example")),
reference_time + i * base::Minutes(1),
PrivateAggregationCallerApi::kProtectedAudience));
}
// Consuming this budget 1440 times in a day would not exceed the daily
// budget, but 25 times would.
int budget_to_use_per_minute =
PrivateAggregationBudgeter::kLargerScopeValues.max_budget_per_scope /
1440;
EXPECT_GT(
budget_to_use_per_minute * 1441,
PrivateAggregationBudgeter::kLargerScopeValues.max_budget_per_scope);
// Make sure this is valid to request within a 10 min period.
EXPECT_LT(
10 * budget_to_use_per_minute,
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope);
// Use budget in the first 1440 larger scopes.
for (int i = 0; i < 1440; ++i) {
ConsumeBudget(budget_to_use_per_minute, example_keys[i],
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
}
// The last 1440 larger scope windows are used for calculating remaining
// budget, so we can't use more during the 1440th.
ConsumeBudget(budget_to_use_per_minute, example_keys[1439],
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result,
RequestResult::kInsufficientLargerScopeBudget);
++num_queries_processed;
}));
base::RunLoop run_loop;
// But the last window can use budget as the first window is no longer in the
// relevant set of 1440 larger scope windows.
ConsumeBudget(budget_to_use_per_minute, example_keys[1440],
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 1442);
}
TEST_P(PrivateAggregationBudgeterTest, ConsumeBudgetDifferentApis) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey protected_audience_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey shared_storage_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kSharedStorage);
ConsumeBudget(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope,
protected_audience_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.
ConsumeBudget(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope,
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_P(PrivateAggregationBudgeterTest, ConsumeBudgetDifferentSites) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey key_a =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey key_b =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://b.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope,
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 site does not interfere with the other.
ConsumeBudget(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope,
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_P(PrivateAggregationBudgeterTest, ConsumeBudgetDifferentOriginsSameSite) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey key_a =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.domain.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey key_b =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://b.domain.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope,
key_a,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
base::RunLoop run_loop;
// The budget is shared for different origins in the same site.
ConsumeBudget(
/*budget=*/1, key_b,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++num_queries_processed;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 2);
}
TEST_P(PrivateAggregationBudgeterTest, ConsumeBudgetValueTooLarge) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
base::RunLoop run_loop;
// Request will be rejected if budget exceeds maximum
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope +
1),
example_key, base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kRequestedMoreThanTotalBudget);
run_loop.Quit();
}));
run_loop.Run();
}
TEST_P(PrivateAggregationBudgeterTest, BudgetValidityMetricsRecorded) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey budget_key = CreateBudgetKey();
constexpr int64_t kLargerScopeDuration =
PrivateAggregationBudgeter::kLargerScopeValues.budget_scope_duration
.InMicroseconds();
constexpr int64_t kWindowDuration =
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 + kWindowDuration - kLargerScopeDuration;
int64_t after_latest_window_start = latest_window_start + kWindowDuration;
int64_t before_oldest_window_start = oldest_window_start - kWindowDuration;
constexpr int kMaxSmallerScopeBudget =
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope;
struct BudgetEntries {
int budget;
int64_t timestamp;
};
enum FatalErrorExpectation { kExpectFatalError, kDontExpectFatalError };
const struct {
BudgetEntryValidityStatus expected_status;
std::vector<BudgetEntries> budgets;
// If there is a fatal error, only the first budgeting call will occur.
FatalErrorExpectation fatal_error_expectation = kDontExpectFatalError;
} 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},
{kMaxSmallerScopeBudget + 1, latest_window_start},
},
kExpectFatalError},
{BudgetEntryValidityStatus::kContainsTimestampNotRoundedToMinute,
{
{1, oldest_window_start},
{kMaxSmallerScopeBudget, latest_window_start + 1},
}},
{BudgetEntryValidityStatus::kContainsNonPositiveValue,
{
{-1, after_latest_window_start},
{3, oldest_window_start + 1},
{kMaxSmallerScopeBudget + 1, latest_window_start},
},
kExpectFatalError},
{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);
}
ConsumeBudget(
/*budget=*/1, budget_key,
base::BindLambdaForTesting([&](RequestResult result) {
DeleteAllBudgetData();
run_loop.Quit();
}));
histograms.ExpectUniqueSample(
"PrivacySandbox.PrivateAggregation.Budgeter.BudgetValidityStatus2",
test_case.expected_status,
GetErrorReportingEnabledParam() &&
test_case.fatal_error_expectation == kDontExpectFatalError
? 2
: 1);
run_loop.Run();
}
}
TEST_F(PrivateAggregationBudgeterErrorReportingDisabledTest,
EnoughBudgetIfNotEnoughOverallMetricRecorded) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey budget_key = CreateBudgetKey();
constexpr int64_t kLargerScopeDuration =
PrivateAggregationBudgeter::kLargerScopeValues.budget_scope_duration
.InMicroseconds();
constexpr int64_t kWindowDuration =
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 + kWindowDuration - kLargerScopeDuration;
constexpr int kMaxSmallerScopeBudget =
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope;
constexpr int kMaxLargerScopeBudget =
PrivateAggregationBudgeter::kLargerScopeValues.max_budget_per_scope;
const struct {
std::string_view description;
std::optional<bool> expected_status;
int amount_to_consume;
int minimum_histogram_value;
int smaller_scope_budget_used;
int larger_scope_budget_used;
} kTestCases[] = {
{
"amount is approved",
/*expected_status=*/std::nullopt,
/*amount_to_consume=*/1000,
/*minimum_histogram_value=*/10,
/*smaller_scope_budget_used=*/0,
/*larger_scope_budget_used=*/0,
},
{
"insufficent small scope budget, but minimum would succeed",
/*expected_status=*/true,
/*amount_to_consume=*/1000,
/*minimum_histogram_value=*/10,
/*smaller_scope_budget_used=*/kMaxSmallerScopeBudget - 100,
/*larger_scope_budget_used=*/0,
},
{
"insufficent small scope budget, minimum would also fail",
/*expected_status=*/false,
/*amount_to_consume=*/1000,
/*minimum_histogram_value=*/10,
/*smaller_scope_budget_used=*/kMaxSmallerScopeBudget - 1,
/*larger_scope_budget_used=*/0,
},
{
"insufficent large scope budget, but minimum would succeed",
/*expected_status=*/true,
/*amount_to_consume=*/1000,
/*minimum_histogram_value=*/10,
/*smaller_scope_budget_used=*/0,
/*larger_scope_budget_used=*/kMaxLargerScopeBudget - 100,
},
{
"insufficent large scope budget, minimum would also fail",
/*expected_status=*/false,
/*amount_to_consume=*/1000,
/*minimum_histogram_value=*/10,
/*smaller_scope_budget_used=*/0,
/*larger_scope_budget_used=*/kMaxLargerScopeBudget - 1,
},
{
"requested more than the total budget",
/*expected_status=*/std::nullopt,
/*amount_to_consume=*/kMaxSmallerScopeBudget + 1,
/*minimum_histogram_value=*/10,
/*smaller_scope_budget_used=*/0,
/*larger_scope_budget_used=*/0,
},
{
"minimum_histogram_value 0",
/*expected_status=*/std::nullopt,
/*amount_to_consume=*/1000,
/*minimum_histogram_value=*/0,
/*smaller_scope_budget_used=*/0,
/*larger_scope_budget_used=*/0,
}};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.description);
base::HistogramTester histograms;
base::RunLoop run_loop;
if (test_case.smaller_scope_budget_used) {
AddBudgetValueAtTimestamp(budget_key, test_case.smaller_scope_budget_used,
latest_window_start);
}
if (test_case.larger_scope_budget_used) {
AddBudgetValueAtTimestamp(budget_key, test_case.larger_scope_budget_used,
oldest_window_start);
}
budgeter()->ConsumeBudget(
/*budget=*/test_case.amount_to_consume, budget_key,
test_case.minimum_histogram_value,
base::BindLambdaForTesting([&](RequestResult result) {
DeleteAllBudgetData();
run_loop.Quit();
}));
constexpr std::string_view kHistogram =
"PrivacySandbox.PrivateAggregation.Budgeter."
"EnoughBudgetForAnyValueIfNotEnoughOverall";
if (test_case.expected_status.has_value()) {
histograms.ExpectUniqueSample(kHistogram,
test_case.expected_status.value(), 1);
} else {
histograms.ExpectTotalCount(kHistogram, 0);
}
run_loop.Run();
}
}
TEST_P(PrivateAggregationBudgeterTest,
ConsumeBudgetBeforeInitialized_QueriesAreQueued) {
base::RunLoop run_loop;
CreateBudgeterWithoutInitializing();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
// Queries should be processed in the order they are received.
int num_queries_processed = 0;
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
EXPECT_EQ(++num_queries_processed, 1);
}));
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
1),
example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
EXPECT_EQ(++num_queries_processed, 2);
}));
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed, &run_loop](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
EXPECT_EQ(++num_queries_processed, 3);
run_loop.Quit();
}));
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_P(PrivateAggregationBudgeterTest,
ClearDataBeforeInitialized_QueriesAreQueued) {
base::RunLoop run_loop;
CreateBudgeterWithoutInitializing();
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);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kInitializing);
run_loop.Run();
EXPECT_TRUE(was_callback_run);
EXPECT_EQ(GetStorageStatus(),
PrivateAggregationBudgeter::StorageStatus::kOpen);
}
TEST_P(PrivateAggregationBudgeterTest,
ConsumeBudgetBeforeFailedInitialization_QueuedQueriesAreRejected) {
// The database initialization will fail to open if its directory already
// exists.
base::CreateDirectory(db_path());
base::RunLoop run_loop;
CreateBudgeterWithoutInitializing();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
// Queries should be processed in the order they are received.
int num_queries_processed = 0;
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kStorageInitializationFailed);
EXPECT_EQ(++num_queries_processed, 1);
}));
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
1),
example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kStorageInitializationFailed);
EXPECT_EQ(++num_queries_processed, 2);
}));
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed, &run_loop](RequestResult result) {
EXPECT_EQ(result, RequestResult::kStorageInitializationFailed);
EXPECT_EQ(++num_queries_processed, 3);
run_loop.Quit();
}));
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_P(PrivateAggregationBudgeterTest,
MaxPendingCallsExceeded_AdditionalConsumeBudgetCallsRejected) {
base::RunLoop run_loop;
CreateBudgeterWithoutInitializing();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
int num_queries_succeeded = 0;
base::RepeatingClosure barrier_quit_closure = base::BarrierClosure(
PrivateAggregationBudgeter::kMaxPendingCalls, run_loop.QuitClosure());
for (int i = 0; i < PrivateAggregationBudgeter::kMaxPendingCalls; ++i) {
// Queries should be processed in the order they are received.
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_succeeded, i,
&barrier_quit_closure](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
EXPECT_EQ(num_queries_succeeded++, i);
barrier_quit_closure.Run();
}));
}
// This query should be immediately rejected.
bool was_callback_run = false;
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_P(PrivateAggregationBudgeterTest,
MaxPendingCallsExceeded_AdditionalDataClearingCallsAllowed) {
base::RunLoop run_loop;
CreateBudgeterWithoutInitializing();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
int num_consume_queries_succeeded = 0;
for (int i = 0; i < PrivateAggregationBudgeter::kMaxPendingCalls; ++i) {
// Queries should be processed in the order they are received.
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_P(PrivateAggregationBudgeterTest,
BudgeterDestroyedImmediatelyAfterCreation_DoesNotCrash) {
CreateBudgeterWithoutInitializing(/*exclusively_run_in_memory=*/false);
DestroyBudgeter();
base::RunLoop().RunUntilIdle();
}
TEST_P(PrivateAggregationBudgeterTest,
BudgeterDestroyedImmediatelyAfterInitializationStarted_DoesNotCrash) {
base::RunLoop run_loop;
CreateBudgeterWithoutInitializing(/*exclusively_run_in_memory=*/false);
InitializeBudgeter(/*on_done_initializing=*/base::DoNothing());
DestroyBudgeter();
base::RunLoop().RunUntilIdle();
}
TEST_P(PrivateAggregationBudgeterTest,
BudgeterDestroyedImmediatelyAfterInitialization_DoesNotCrash) {
base::RunLoop run_loop;
CreateBudgeterWithoutInitializing(/*exclusively_run_in_memory=*/false);
InitializeBudgeter(
/*on_done_initializing=*/base::BindLambdaForTesting([this, &run_loop]() {
DestroyBudgeter();
run_loop.Quit();
}));
run_loop.Run();
}
TEST_P(PrivateAggregationBudgeterTest, ClearDataBasicTest) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
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.
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++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
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key, base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
}
TEST_P(PrivateAggregationBudgeterTest, ClearDataCrossesWindowBoundary) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key_1 =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey example_key_2 =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
kExampleTime + PrivateAggregationBudgetKey::TimeWindow::kDuration,
PrivateAggregationCallerApi::kProtectedAudience);
EXPECT_NE(example_key_1.time_window().start_time(),
example_key_2.time_window().start_time());
ConsumeBudget(
/*budget=*/1, example_key_1,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
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.
ConsumeBudget(
/*budget=*/1, example_key_2,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++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.
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
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_P(PrivateAggregationBudgeterTest,
ClearDataDoesntAffectWindowsOutsideRange) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey key_to_clear =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey key_after =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
kExampleTime + PrivateAggregationBudgetKey::TimeWindow::kDuration,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey key_before =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")),
kExampleTime - PrivateAggregationBudgetKey::TimeWindow::kDuration,
PrivateAggregationCallerApi::kProtectedAudience);
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;
});
ConsumeBudget(
/*budget=*/1, key_before, expect_approved);
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
2),
key_to_clear, expect_approved);
ConsumeBudget(
/*budget=*/1, key_after, expect_approved);
// The full budget has been used across the three time windows.
ConsumeBudget(
/*budget=*/1, key_after,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++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::kSmallerScopeValues.max_budget_per_scope -
// 2)` that we can use.
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
2),
key_after, expect_approved);
ConsumeBudget(
/*budget=*/1, key_after,
base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 7);
}
TEST_P(PrivateAggregationBudgeterTest, ClearDataAllApisAffected) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey protected_audience_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey shared_storage_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::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::kInsufficientSmallerScopeBudget);
++num_queries_processed;
});
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
protected_audience_key, expect_approved);
// Maximum budget has been used so this should fail.
ConsumeBudget(
/*budget=*/1, protected_audience_key, expect_insufficient_budget);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
shared_storage_key, expect_approved);
// Maximum budget has been used so this should fail.
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
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
protected_audience_key, expect_approved);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
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_P(PrivateAggregationBudgeterTest, ClearAllDataBasicTest) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
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.
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++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
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key, base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
}
TEST_P(PrivateAggregationBudgeterTest, ClearAllDataNullTimes) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
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.
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++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
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key, base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
}
TEST_P(PrivateAggregationBudgeterTest, ClearAllDataNullStartNonNullEndTime) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
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.
ConsumeBudget(
/*budget=*/1, example_key,
base::BindLambdaForTesting(
[&num_queries_processed](RequestResult result) {
EXPECT_EQ(result, RequestResult::kInsufficientSmallerScopeBudget);
++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
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key, base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
++num_queries_processed;
}));
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
}
TEST_P(PrivateAggregationBudgeterTest, ClearDataFilterSelectsOrigins) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
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,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey example_key_b =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginB, kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
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::kInsufficientSmallerScopeBudget);
++num_queries_processed;
});
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_a, expect_approved);
// Maximum budget has been used so this should fail.
ConsumeBudget(
/*budget=*/1, example_key_a, expect_insufficient_budget);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_b, expect_approved);
// Maximum budget has been used so this should fail.
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::CreateFirstParty(kOriginA);
}),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again for the cleared origin.
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_a, expect_approved);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_b, expect_insufficient_budget);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 7);
}
TEST_P(PrivateAggregationBudgeterTest, ClearDataAllTimeFilterSelectsOrigins) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
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,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey example_key_b =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginB, kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
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::kInsufficientSmallerScopeBudget);
++num_queries_processed;
});
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_a, expect_approved);
// Maximum budget has been used so this should fail.
ConsumeBudget(
/*budget=*/1, example_key_a, expect_insufficient_budget);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_b, expect_approved);
// Maximum budget has been used so this should fail.
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::CreateFirstParty(kOriginA);
}),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again for the cleared origin.
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_a, expect_approved);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_b, expect_insufficient_budget);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 7);
}
TEST_P(PrivateAggregationBudgeterTest, ClearDataUnusedSameSiteOrigin) {
base::HistogramTester histogram;
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
const url::Origin kOriginA =
url::Origin::Create(GURL("https://a.domain.example/"));
const url::Origin kOriginB =
url::Origin::Create(GURL("https://b.domain.example/"));
PrivateAggregationBudgetKey example_key_a =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginA, kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey example_key_b =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginB, kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
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::kInsufficientSmallerScopeBudget);
++num_queries_processed;
});
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_a, expect_approved);
// Maximum budget has been used so this should fail. This should not record
// kOriginB in the budget storage.
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::CreateFirstParty(kOriginB);
}),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// Nothing should've been cleared as the origin was not recorded, so attempts
// to use budget should still fail.
ConsumeBudget(
/*budget=*/1, example_key_a, expect_insufficient_budget);
ConsumeBudget(
/*budget=*/1, example_key_b, expect_insufficient_budget);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 5);
histogram.ExpectUniqueSample(
"PrivacySandbox.PrivateAggregation.Budgeter."
"NumReportingOriginsStoredPerSite",
/*sample=*/1, /*expected_bucket_count=*/4);
}
TEST_P(PrivateAggregationBudgeterTest, ClearDataSameSiteOriginUsed) {
base::HistogramTester histogram;
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
const url::Origin kOriginA =
url::Origin::Create(GURL("https://a.domain.example/"));
const url::Origin kOriginB =
url::Origin::Create(GURL("https://b.domain.example/"));
PrivateAggregationBudgetKey example_key_a =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginA, kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey example_key_b =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginB, kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
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::kInsufficientSmallerScopeBudget);
++num_queries_processed;
});
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
1),
example_key_a, expect_approved);
ConsumeBudget(
/*budget=*/1, example_key_b, expect_approved);
// `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::CreateFirstParty(kOriginB);
}),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again for the cleared site as
// both origins were recorded.
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_a, expect_approved);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 4);
histogram.ExpectTotalCount(
"PrivacySandbox.PrivateAggregation.Budgeter."
"NumReportingOriginsStoredPerSite",
/*expected_count=*/3);
histogram.ExpectBucketCount(
"PrivacySandbox.PrivateAggregation.Budgeter."
"NumReportingOriginsStoredPerSite",
/*sample=*/1, /*expected_count=*/1);
histogram.ExpectBucketCount(
"PrivacySandbox.PrivateAggregation.Budgeter."
"NumReportingOriginsStoredPerSite",
/*sample=*/2, /*expected_count=*/2);
}
TEST_P(PrivateAggregationBudgeterTest,
ClearDataOriginLastUsedBeforeDeletionWindow) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
const url::Origin kOriginA =
url::Origin::Create(GURL("https://a.domain.example/"));
const url::Origin kOriginB =
url::Origin::Create(GURL("https://b.domain.example/"));
PrivateAggregationBudgetKey example_key_a =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginA, kExampleTime - base::Minutes(5),
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey example_key_b =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginB, kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
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::kInsufficientSmallerScopeBudget);
++num_queries_processed;
});
ConsumeBudget(
/*budget=*/(
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
1),
example_key_a, expect_approved);
ConsumeBudget(
/*budget=*/1, example_key_b, expect_approved);
// Maximum budget has been used so this should fail.
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 - base::Minutes(4), kExampleTime,
base::BindLambdaForTesting([&](const blink::StorageKey& storage_key) {
return storage_key == blink::StorageKey::CreateFirstParty(kOriginA);
}),
base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// The clearing should have deleted nothing as kOriginA was last used before
// the deletion window.
ConsumeBudget(
/*budget=*/1, example_key_b, expect_insufficient_budget);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 5);
}
TEST_P(PrivateAggregationBudgeterTest,
BudgeterDestroyedImmedatelyAfterClearData_CallbackStillRun) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
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);
}
TEST_P(PrivateAggregationBudgeterTest, ClearDataByDataKey) {
int num_queries_processed = 0;
CreateAndInitializeBudgeterThenWait();
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,
PrivateAggregationCallerApi::kProtectedAudience);
PrivateAggregationBudgetKey example_key_b =
PrivateAggregationBudgetKey::CreateForTesting(
kOriginB, kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
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::kInsufficientSmallerScopeBudget);
++num_queries_processed;
});
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_a, expect_approved);
// Maximum budget has been used so this should fail.
ConsumeBudget(
/*budget=*/1, example_key_a, expect_insufficient_budget);
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_b, expect_approved);
// Maximum budget has been used so this should fail.
ConsumeBudget(
/*budget=*/1, example_key_b, expect_insufficient_budget);
std::set<PrivateAggregationDataModel::DataKey> keys;
budgeter()->GetAllDataKeys(base::BindLambdaForTesting(
[&keys](std::set<PrivateAggregationDataModel::DataKey> data_keys) {
keys = std::move(data_keys);
}));
ASSERT_THAT(keys, testing::SizeIs(2));
base::RunLoop run_loop;
budgeter()->DeleteByDataKey(*keys.begin(), base::BindLambdaForTesting([&]() {
++num_queries_processed;
run_loop.Quit();
}));
// After clearing, we can use the full budget again for the cleared origin.
// Other origins aren't affected.
ConsumeBudget(
/*budget=*/PrivateAggregationBudgeter::kSmallerScopeValues
.max_budget_per_scope,
example_key_a, expect_approved);
ConsumeBudget(/*budget=*/1, example_key_b, expect_insufficient_budget);
run_loop.Run();
EXPECT_EQ(num_queries_processed, 7);
}
TEST_P(PrivateAggregationBudgeterTest, StaleDataClearedOnInitialization) {
CreateAndInitializeBudgeterThenWait();
int64_t latest_window_start =
PrivateAggregationBudgetKey::TimeWindow(base::Time::Now())
.start_time()
.ToDeltaSinceWindowsEpoch()
.InMicroseconds();
int64_t stale_window_start =
latest_window_start - PrivateAggregationBudgeter::kLargerScopeValues
.budget_scope_duration.InMicroseconds();
AddBudgetValueAtTimestamp(CreateBudgetKey(), /*value=*/1,
/*timestamp=*/stale_window_start);
EXPECT_EQ(NumberOfSitesInStorage(), 1);
// Ensure database has a chance to persist storage.
EnsureDbFlushes();
DestroyBudgeter();
CreateAndInitializeBudgeterThenWait();
// Ensure the (zero-delay) clean up task has a chance to run.
task_environment_.FastForwardBy(base::TimeDelta());
// The stale data should've been cleared.
EXPECT_EQ(NumberOfSitesInStorage(), 0);
}
TEST_P(PrivateAggregationBudgeterTest, StaleDataClearedAfterConsumeBudget) {
CreateAndInitializeBudgeterThenWait();
int64_t latest_window_start =
PrivateAggregationBudgetKey::TimeWindow(base::Time::Now())
.start_time()
.ToDeltaSinceWindowsEpoch()
.InMicroseconds();
int64_t stale_window_start =
latest_window_start - PrivateAggregationBudgeter::kLargerScopeValues
.budget_scope_duration.InMicroseconds();
AddBudgetValueAtTimestamp(CreateBudgetKey(), /*value=*/1,
/*timestamp=*/stale_window_start);
PrivateAggregationBudgetKey non_stale_key =
PrivateAggregationBudgetKey::CreateForTesting(
/*origin=*/url::Origin::Create(GURL("https://b.example/")),
/*api_invocation_time=*/kExampleTime,
/*caller_api=*/PrivateAggregationCallerApi::kProtectedAudience);
base::RunLoop run_loop;
ConsumeBudget(
/*budget=*/1, non_stale_key,
/*on_done=*/base::BindLambdaForTesting([&](RequestResult result) {
EXPECT_EQ(result, RequestResult::kApproved);
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(NumberOfSitesInStorage(), 2);
task_environment_.FastForwardBy(
PrivateAggregationBudgeter::kMinStaleDataCleanUpGap);
// The stale data should've been cleared.
EXPECT_EQ(NumberOfSitesInStorage(), 1);
}
TEST_F(PrivateAggregationBudgeterErrorReportingEnabledTest,
LockedBudgeterQueuesOtherCalls) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
std::vector<blink::mojom::AggregatableReportHistogramContribution>
example_contribution_vector = {
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/0, /*value=*/1, /*filtering_id=*/std::nullopt)};
base::RunLoop run_loop_1;
std::optional<InspectBudgetCallResult> extracted_result;
int num_successful = 0;
budgeter()->InspectBudgetAndLock(
example_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
extracted_result = std::move(result);
++num_successful;
run_loop_1.Quit();
}));
run_loop_1.Run();
EXPECT_EQ(num_successful, 1u);
ASSERT_TRUE(extracted_result.has_value());
base::RunLoop run_loop_2;
base::RepeatingClosure barrier =
base::BarrierClosure(2, run_loop_2.QuitClosure());
budgeter()->ClearData(base::Time::Min(), base::Time::Max(),
StoragePartition::StorageKeyMatcherFunction(),
base::BindLambdaForTesting([&]() {
++num_successful;
barrier.Run();
}));
budgeter()->InspectBudgetAndLock(
example_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
++num_successful;
barrier.Run();
}));
// All subsequent calls should be queued as the budgeter is locked.
EXPECT_EQ(num_successful, 1u);
budgeter()->ConsumeBudget(
std::move(extracted_result->lock.value()), example_contribution_vector,
example_key, base::BindLambdaForTesting([&](BudgetQueryResult result) {
++num_successful;
}));
// Note: the `ConsumeBudget()` callback will be run before the queued tasks
// are run.
run_loop_2.Run();
EXPECT_EQ(num_successful, 4u);
}
TEST_F(PrivateAggregationBudgeterErrorReportingEnabledTest,
MultipleContributionsInspected_AllApproved) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
using Contribution = blink::mojom::AggregatableReportHistogramContribution;
std::vector<Contribution> example_contribution_vector = {
Contribution(/*bucket=*/12, /*value=*/34, /*filtering_id=*/std::nullopt),
Contribution(/*bucket=*/56, /*value=*/78, /*filtering_id=*/9),
Contribution(/*bucket=*/12, /*value=*/12, /*filtering_id=*/3)};
base::RunLoop run_loop;
std::optional<PrivateAggregationBudgeter::Lock> extracted_lock;
budgeter()->InspectBudgetAndLock(
example_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
EXPECT_EQ(result.query_result.overall_result, RequestResult::kApproved);
EXPECT_EQ(result.query_result.result_for_each_contribution,
std::vector<ResultForContribution>(
3, ResultForContribution::kApproved));
extracted_lock = std::move(result.lock);
run_loop.Quit();
}));
run_loop.Run();
budgeter()->ConsumeBudget(
std::move(extracted_lock).value(), example_contribution_vector,
example_key, base::BindLambdaForTesting([&](BudgetQueryResult result) {
EXPECT_EQ(result.overall_result, RequestResult::kApproved);
EXPECT_EQ(result.result_for_each_contribution,
std::vector<ResultForContribution>(
3, ResultForContribution::kApproved));
}));
}
TEST_F(PrivateAggregationBudgeterErrorReportingEnabledTest,
MultipleContributionsInspected_SomeDenied) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
using Contribution = blink::mojom::AggregatableReportHistogramContribution;
std::vector<Contribution> example_contribution_vector = {
Contribution(/*bucket=*/12, /*value=*/2, /*filtering_id=*/std::nullopt),
Contribution(
/*bucket=*/34, /*value=*/
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope -
1,
/*filtering_id=*/5),
Contribution(/*bucket=*/67, /*value=*/8, /*filtering_id=*/9)};
base::RunLoop run_loop;
std::optional<PrivateAggregationBudgeter::Lock> extracted_lock;
budgeter()->InspectBudgetAndLock(
example_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
EXPECT_EQ(result.query_result.overall_result,
RequestResult::kRequestedMoreThanTotalBudget);
EXPECT_EQ(result.query_result.result_for_each_contribution,
std::vector<ResultForContribution>(
{ResultForContribution::kApproved,
ResultForContribution::kDenied,
ResultForContribution::kApproved}));
extracted_lock = std::move(result.lock);
run_loop.Quit();
}));
run_loop.Run();
budgeter()->ConsumeBudget(
std::move(extracted_lock).value(), example_contribution_vector,
example_key, base::BindLambdaForTesting([&](BudgetQueryResult result) {
EXPECT_EQ(result.overall_result,
RequestResult::kRequestedMoreThanTotalBudget);
EXPECT_EQ(result.result_for_each_contribution,
std::vector<ResultForContribution>(
{ResultForContribution::kApproved,
ResultForContribution::kDenied,
ResultForContribution::kApproved}));
}));
}
TEST_F(PrivateAggregationBudgeterErrorReportingEnabledTest,
LockHoldHistogramRecordedOnConsumeBudget) {
CreateAndInitializeBudgeterThenWait();
base::HistogramTester histogram_tester;
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
std::vector<blink::mojom::AggregatableReportHistogramContribution>
example_contribution_vector = {
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/123, /*value=*/45, /*filtering_id=*/std::nullopt)};
base::RunLoop run_loop_1;
std::optional<PrivateAggregationBudgeter::Lock> extracted_lock;
budgeter()->InspectBudgetAndLock(
example_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
extracted_lock = std::move(result.lock);
run_loop_1.Quit();
}));
run_loop_1.Run();
task_environment_.AdvanceClock(base::Seconds(1));
base::RunLoop run_loop_2;
budgeter()->ConsumeBudget(
std::move(extracted_lock).value(), example_contribution_vector,
example_key, base::BindLambdaForTesting([&](BudgetQueryResult result) {
run_loop_2.Quit();
}));
run_loop_2.Run();
histogram_tester.ExpectUniqueSample(
"PrivacySandbox.PrivateAggregation.Budgeter.LockHoldDuration",
base::Seconds(1).InMilliseconds(), 1);
}
TEST_F(PrivateAggregationBudgeterErrorReportingEnabledTest,
LockHoldHistogramRecordedOnBudgetDestruction) {
CreateAndInitializeBudgeterThenWait();
base::HistogramTester histogram_tester;
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
std::vector<blink::mojom::AggregatableReportHistogramContribution>
example_contribution_vector = {
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/123, /*value=*/45, /*filtering_id=*/std::nullopt)};
base::RunLoop run_loop;
std::optional<PrivateAggregationBudgeter::Lock> extracted_lock;
budgeter()->InspectBudgetAndLock(
example_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
extracted_lock = std::move(result.lock);
run_loop.Quit();
}));
run_loop.Run();
task_environment_.AdvanceClock(base::Seconds(1));
DestroyBudgeter();
histogram_tester.ExpectUniqueSample(
"PrivacySandbox.PrivateAggregation.Budgeter.LockHoldDuration",
base::Seconds(1).InMilliseconds(), 1);
}
TEST_F(PrivateAggregationBudgeterErrorReportingEnabledTest,
EmptyContributionsInspected_Approved) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
std::vector<blink::mojom::AggregatableReportHistogramContribution>
empty_contribution_vector = {};
base::RunLoop run_loop;
std::optional<PrivateAggregationBudgeter::Lock> extracted_lock;
budgeter()->InspectBudgetAndLock(
empty_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
EXPECT_EQ(result.query_result.overall_result, RequestResult::kApproved);
EXPECT_THAT(result.query_result.result_for_each_contribution,
testing::IsEmpty());
extracted_lock = std::move(result.lock);
run_loop.Quit();
}));
run_loop.Run();
budgeter()->ConsumeBudget(
std::move(extracted_lock).value(), empty_contribution_vector, example_key,
base::BindLambdaForTesting([&](BudgetQueryResult result) {
EXPECT_EQ(result.overall_result, RequestResult::kApproved);
EXPECT_THAT(result.result_for_each_contribution, testing::IsEmpty());
}));
}
TEST_F(PrivateAggregationBudgeterErrorReportingEnabledTest,
MultipleContributionsWithSumExceeding32Bits) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
// Value sums to value 1 if 32-bit overflow is allowed.
using Contribution = blink::mojom::AggregatableReportHistogramContribution;
std::vector<Contribution> example_contribution_vector = {
Contribution(/*bucket=*/12, /*value=*/3, /*filtering_id=*/std::nullopt),
Contribution(/*bucket=*/12, /*value=*/std::numeric_limits<int32_t>::max(),
/*filtering_id=*/std::nullopt),
Contribution(/*bucket=*/12, /*value=*/std::numeric_limits<int32_t>::max(),
/*filtering_id=*/std::nullopt)};
base::RunLoop run_loop;
std::optional<PrivateAggregationBudgeter::Lock> extracted_lock;
budgeter()->InspectBudgetAndLock(
example_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
EXPECT_EQ(result.query_result.overall_result,
RequestResult::kRequestedMoreThanTotalBudget);
EXPECT_EQ(result.query_result.result_for_each_contribution,
std::vector<ResultForContribution>(
{ResultForContribution::kApproved,
ResultForContribution::kDenied,
ResultForContribution::kDenied}));
extracted_lock = std::move(result.lock);
run_loop.Quit();
}));
run_loop.Run();
budgeter()->ConsumeBudget(
std::move(extracted_lock).value(),
{Contribution(/*bucket=*/12, /*value=*/3, /*filtering_id=*/std::nullopt)},
example_key, base::BindLambdaForTesting([&](BudgetQueryResult result) {
EXPECT_EQ(result.overall_result, RequestResult::kApproved);
EXPECT_EQ(result.result_for_each_contribution,
std::vector<ResultForContribution>(
{ResultForContribution::kApproved}));
}));
}
TEST_F(PrivateAggregationBudgeterErrorReportingEnabledTest,
DiskUsageAlreadyExceedsBudget_BadValuesOnDisk) {
CreateAndInitializeBudgeterThenWait();
PrivateAggregationBudgetKey example_key =
PrivateAggregationBudgetKey::CreateForTesting(
url::Origin::Create(GURL("https://a.example/")), kExampleTime,
PrivateAggregationCallerApi::kProtectedAudience);
int64_t latest_window_start = example_key.time_window()
.start_time()
.ToDeltaSinceWindowsEpoch()
.InMicroseconds();
int64_t previous_window_start =
(example_key.time_window().start_time() -
PrivateAggregationBudgetKey::TimeWindow::kDuration)
.ToDeltaSinceWindowsEpoch()
.InMicroseconds();
AddBudgetValueAtTimestamp(
example_key,
PrivateAggregationBudgeter::kSmallerScopeValues.max_budget_per_scope,
latest_window_start);
AddBudgetValueAtTimestamp(example_key, 1, previous_window_start);
using Contribution = blink::mojom::AggregatableReportHistogramContribution;
std::vector<Contribution> example_contribution_vector = {
Contribution(/*bucket=*/12, /*value=*/1, /*filtering_id=*/std::nullopt)};
base::RunLoop run_loop;
budgeter()->InspectBudgetAndLock(
example_contribution_vector, example_key,
base::BindLambdaForTesting([&](InspectBudgetCallResult result) {
EXPECT_EQ(result.query_result.overall_result,
RequestResult::kBadValuesOnDisk);
EXPECT_TRUE(result.query_result.result_for_each_contribution.empty());
EXPECT_FALSE(result.lock.has_value());
run_loop.Quit();
}));
run_loop.Run();
}
} // namespace
} // namespace content