| // 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_manager_impl.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/time/time.h" |
| #include "content/browser/aggregation_service/aggregatable_report.h" |
| #include "content/browser/aggregation_service/aggregation_service.h" |
| #include "content/browser/aggregation_service/aggregation_service_test_utils.h" |
| #include "content/browser/private_aggregation/private_aggregation_budget_key.h" |
| #include "content/browser/private_aggregation/private_aggregation_budgeter.h" |
| #include "content/browser/private_aggregation/private_aggregation_features.h" |
| #include "content/browser/private_aggregation/private_aggregation_host.h" |
| #include "content/browser/private_aggregation/private_aggregation_test_utils.h" |
| #include "content/public/browser/private_aggregation_data_model.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/mojom/private_aggregation/aggregatable_report.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| using BudgetDeniedBehavior = PrivateAggregationBudgeter::BudgetDeniedBehavior; |
| |
| using testing::_; |
| using testing::Invoke; |
| using testing::Return; |
| |
| using Checkpoint = testing::MockFunction<void(int step)>; |
| |
| constexpr auto kExampleTime = |
| base::Time::FromMillisecondsSinceUnixEpoch(1652984901234); |
| |
| constexpr char kExampleOriginUrl[] = "https://origin.example"; |
| constexpr char kExampleMainFrameUrl[] = "https://main_frame.example"; |
| constexpr char kExampleCoordinatorUrl[] = "https://coordinator.example"; |
| |
| class PrivateAggregationManagerImplUnderTest |
| : public PrivateAggregationManagerImpl { |
| public: |
| explicit PrivateAggregationManagerImplUnderTest( |
| std::unique_ptr<PrivateAggregationBudgeter> budgeter, |
| std::unique_ptr<PrivateAggregationHost> host, |
| std::unique_ptr<AggregationService> aggregation_service) |
| : PrivateAggregationManagerImpl(std::move(budgeter), |
| std::move(host), |
| /*storage_partition=*/nullptr), |
| aggregation_service_(std::move(aggregation_service)) {} |
| |
| using PrivateAggregationManagerImpl::OnReportRequestDetailsReceivedFromHost; |
| |
| AggregationService* GetAggregationService() override { |
| return aggregation_service_.get(); |
| } |
| |
| private: |
| std::unique_ptr<AggregationService> aggregation_service_; |
| }; |
| |
| // Returns a generator and contributions vector. The generator returns a clone |
| // of `request` but must be passed the corresponding contributions vector. |
| // Used for manually triggering `OnReportRequestDetailsReceivedFromHost()`. |
| std::pair<PrivateAggregationHost::ReportRequestGenerator, |
| std::vector<blink::mojom::AggregatableReportHistogramContribution>> |
| CloneAndSplitOutGenerator(const AggregatableReportRequest& request) { |
| AggregatableReportRequest clone = |
| aggregation_service::CloneReportRequest(request); |
| |
| PrivateAggregationHost::ReportRequestGenerator fake_generator = |
| base::BindOnce( |
| [](AggregatableReportRequest clone, |
| std::vector<blink::mojom::AggregatableReportHistogramContribution> |
| contributions) { |
| // Handle null reports |
| if (contributions.empty()) { |
| contributions.emplace_back(/*bucket=*/0, /*value=*/0); |
| } |
| EXPECT_EQ(contributions, clone.payload_contents().contributions); |
| return clone; |
| }, |
| std::move(clone)); |
| return std::make_pair(std::move(fake_generator), |
| request.payload_contents().contributions); |
| } |
| |
| constexpr char kBudgeterResultHistogram[] = |
| "PrivacySandbox.PrivateAggregation.Budgeter.RequestResult3"; |
| |
| constexpr char kManagerResultHistogram[] = |
| "PrivacySandbox.PrivateAggregation.Manager.RequestResult"; |
| |
| } // namespace |
| |
| class PrivateAggregationManagerImplTest : public testing::Test { |
| public: |
| PrivateAggregationManagerImplTest() |
| : budgeter_(new testing::StrictMock<MockPrivateAggregationBudgeter>()), |
| host_(new testing::StrictMock<MockPrivateAggregationHost>()), |
| aggregation_service_(new testing::StrictMock<MockAggregationService>()), |
| manager_(base::WrapUnique(budgeter_.get()), |
| base::WrapUnique(host_.get()), |
| base::WrapUnique(aggregation_service_.get())) {} |
| |
| ~PrivateAggregationManagerImplTest() override { |
| budgeter_ = nullptr; |
| host_ = nullptr; |
| aggregation_service_ = nullptr; |
| } |
| |
| protected: |
| BrowserTaskEnvironment task_environment_; |
| |
| // Keep pointers around for EXPECT_CALL. |
| raw_ptr<MockPrivateAggregationBudgeter> budgeter_; |
| raw_ptr<MockPrivateAggregationHost> host_; |
| raw_ptr<MockAggregationService> aggregation_service_; |
| |
| testing::StrictMock<PrivateAggregationManagerImplUnderTest> manager_; |
| }; |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| BasicReportRequest_FerriedAppropriately) { |
| base::HistogramTester histogram; |
| |
| const url::Origin example_origin = |
| url::Origin::Create(GURL(kExampleOriginUrl)); |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| AggregatableReportRequest expected_request = |
| aggregation_service::CreateExampleRequest(); |
| ASSERT_EQ(expected_request.payload_contents().contributions.size(), 1u); |
| |
| // As this report doesn't have debug mode enabled, we shouldn't get any debug |
| // reports. |
| EXPECT_EQ(expected_request.shared_info().debug_mode, |
| AggregatableReportSharedInfo::DebugMode::kDisabled); |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport).Times(0); |
| |
| Checkpoint checkpoint; |
| { |
| testing::InSequence seq; |
| EXPECT_CALL(checkpoint, Call(0)); |
| EXPECT_CALL(*budgeter_, |
| ConsumeBudget( |
| expected_request.payload_contents().contributions[0].value, |
| example_key, _)) |
| .WillOnce(Invoke( |
| [&checkpoint]( |
| int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void( |
| PrivateAggregationBudgeter::RequestResult)> on_done) { |
| checkpoint.Call(1); |
| std::move(on_done).Run( |
| PrivateAggregationBudgeter::RequestResult::kApproved); |
| })); |
| EXPECT_CALL(checkpoint, Call(1)); |
| EXPECT_CALL(*aggregation_service_, ScheduleReport) |
| .WillOnce(Invoke( |
| [&expected_request](AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, expected_request)); |
| })); |
| } |
| |
| checkpoint.Call(0); |
| |
| auto [generator, contributions] = CloneAndSplitOutGenerator(expected_request); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), example_key, |
| BudgetDeniedBehavior::kDontSendReport); |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kApproved, 1); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kSentWithContributions, 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| ReportRequestWithMultipleContributions_CorrectBudgetRequested) { |
| base::HistogramTester histogram; |
| |
| const url::Origin example_origin = |
| url::Origin::Create(GURL(kExampleOriginUrl)); |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.contributions = { |
| blink::mojom::AggregatableReportHistogramContribution(/*bucket=*/123, |
| /*value=*/100), |
| blink::mojom::AggregatableReportHistogramContribution(/*bucket=*/123, |
| /*value=*/5), |
| blink::mojom::AggregatableReportHistogramContribution(/*bucket=*/456, |
| /*value=*/20)}; |
| |
| AggregatableReportRequest expected_request = |
| AggregatableReportRequest::Create(payload_contents, |
| example_request.shared_info().Clone()) |
| .value(); |
| |
| Checkpoint checkpoint; |
| { |
| testing::InSequence seq; |
| |
| EXPECT_CALL(checkpoint, Call(0)); |
| EXPECT_CALL(*budgeter_, ConsumeBudget(/*budget=*/125, example_key, _)) |
| .WillOnce(Invoke( |
| [&checkpoint]( |
| int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void( |
| PrivateAggregationBudgeter::RequestResult)> on_done) { |
| checkpoint.Call(1); |
| std::move(on_done).Run( |
| PrivateAggregationBudgeter::RequestResult::kApproved); |
| })); |
| EXPECT_CALL(checkpoint, Call(1)); |
| EXPECT_CALL(*aggregation_service_, ScheduleReport) |
| .WillOnce(Invoke( |
| [&expected_request](AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, expected_request)); |
| })); |
| } |
| |
| checkpoint.Call(0); |
| |
| auto [generator, contributions] = CloneAndSplitOutGenerator(expected_request); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), example_key, |
| BudgetDeniedBehavior::kDontSendReport); |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kApproved, 1); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kSentWithContributions, 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| BudgetRequestRejected_RequestNotScheduled) { |
| base::HistogramTester histogram; |
| |
| const url::Origin example_origin = |
| url::Origin::Create(GURL(kExampleOriginUrl)); |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| AggregatableReportRequest expected_request = |
| aggregation_service::CreateExampleRequest(); |
| ASSERT_EQ(expected_request.payload_contents().contributions.size(), 1u); |
| |
| Checkpoint checkpoint; |
| { |
| testing::InSequence seq; |
| |
| EXPECT_CALL(checkpoint, Call(0)); |
| EXPECT_CALL(*budgeter_, |
| ConsumeBudget( |
| expected_request.payload_contents().contributions[0].value, |
| example_key, _)) |
| .WillOnce(Invoke( |
| [&checkpoint]( |
| int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void( |
| PrivateAggregationBudgeter::RequestResult)> on_done) { |
| checkpoint.Call(1); |
| std::move(on_done).Run(PrivateAggregationBudgeter::RequestResult:: |
| kInsufficientSmallerScopeBudget); |
| })); |
| EXPECT_CALL(checkpoint, Call(1)); |
| EXPECT_CALL(*aggregation_service_, ScheduleReport).Times(0); |
| } |
| |
| checkpoint.Call(0); |
| |
| auto [generator, contributions] = CloneAndSplitOutGenerator(expected_request); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), example_key, |
| BudgetDeniedBehavior::kDontSendReport); |
| |
| histogram.ExpectUniqueSample(kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult:: |
| kInsufficientSmallerScopeBudget, |
| 1); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kNotSent, 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| BudgetExceedsIntegerLimits_BudgetRejectedWithoutRequest) { |
| base::HistogramTester histogram; |
| |
| const url::Origin example_origin = |
| url::Origin::Create(GURL(kExampleOriginUrl)); |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.contributions = { |
| blink::mojom::AggregatableReportHistogramContribution( |
| /*bucket=*/123, |
| /*value=*/std::numeric_limits<int>::max()), |
| blink::mojom::AggregatableReportHistogramContribution(/*bucket=*/456, |
| /*value=*/1)}; |
| |
| AggregatableReportRequest expected_request = |
| AggregatableReportRequest::Create(payload_contents, |
| example_request.shared_info().Clone()) |
| .value(); |
| |
| EXPECT_CALL(*budgeter_, ConsumeBudget).Times(0); |
| EXPECT_CALL(*aggregation_service_, ScheduleReport).Times(0); |
| |
| auto [generator, contributions] = CloneAndSplitOutGenerator(expected_request); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), example_key, |
| BudgetDeniedBehavior::kDontSendReport); |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kRequestedMoreThanTotalBudget, |
| 1); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kNotSent, 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| DebugRequest_ImmediatelySentAfterBudgetRequest) { |
| base::HistogramTester histogram; |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| shared_info.debug_mode = AggregatableReportSharedInfo::DebugMode::kEnabled; |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_request.shared_info().reporting_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| std::optional<AggregatableReportRequest> standard_request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), shared_info.Clone(), |
| /*reporting_path=*/"/example-reporting-path"); |
| std::optional<AggregatableReportRequest> expected_debug_request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), std::move(shared_info), |
| /*reporting_path=*/ |
| "/.well-known/private-aggregation/debug/report-protected-audience"); |
| ASSERT_TRUE(standard_request.has_value()); |
| ASSERT_TRUE(expected_debug_request.has_value()); |
| |
| EXPECT_CALL( |
| *budgeter_, |
| ConsumeBudget(standard_request->payload_contents().contributions[0].value, |
| example_key, _)) |
| .WillOnce(Invoke( |
| [](int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void(PrivateAggregationBudgeter::RequestResult)> |
| on_done) { |
| std::move(on_done).Run( |
| PrivateAggregationBudgeter::RequestResult::kApproved); |
| })); |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport) |
| .WillOnce(Invoke([&](AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, expected_debug_request.value())); |
| })); |
| |
| // Still triggers the standard (non-debug) report. |
| EXPECT_CALL(*aggregation_service_, ScheduleReport) |
| .WillOnce( |
| Invoke([&standard_request](AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, standard_request.value())); |
| })); |
| |
| auto [generator, contributions] = |
| CloneAndSplitOutGenerator(standard_request.value()); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), example_key, |
| BudgetDeniedBehavior::kDontSendReport); |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kApproved, 1); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kSentWithContributions, 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| DebugRequestWithContextId_ImmediatelySentAfterBudgetRequest) { |
| base::HistogramTester histogram; |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| shared_info.debug_mode = AggregatableReportSharedInfo::DebugMode::kEnabled; |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_request.shared_info().reporting_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| std::optional<AggregatableReportRequest> standard_request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), shared_info.Clone(), |
| /*reporting_path=*/"/example-reporting-path", |
| /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{{"context_id", "example_context_id"}}); |
| std::optional<AggregatableReportRequest> expected_debug_request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), std::move(shared_info), |
| /*reporting_path=*/ |
| "/.well-known/private-aggregation/debug/report-protected-audience", |
| /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{{"context_id", "example_context_id"}}); |
| ASSERT_TRUE(standard_request.has_value()); |
| ASSERT_TRUE(expected_debug_request.has_value()); |
| |
| EXPECT_CALL( |
| *budgeter_, |
| ConsumeBudget(standard_request->payload_contents().contributions[0].value, |
| example_key, _)) |
| .WillOnce(base::test::RunOnceCallback<2>( |
| PrivateAggregationBudgeter::RequestResult::kApproved)); |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport) |
| .WillOnce(Invoke([&](AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, expected_debug_request.value())); |
| })); |
| |
| // Still triggers the standard (non-debug) report. |
| EXPECT_CALL(*aggregation_service_, ScheduleReport) |
| .WillOnce( |
| Invoke([&standard_request](AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, standard_request.value())); |
| })); |
| |
| auto [generator, contributions] = |
| CloneAndSplitOutGenerator(standard_request.value()); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), example_key, |
| BudgetDeniedBehavior::kSendNullReport); |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kApproved, 1); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kSentWithContributions, 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, DebugReportingPath) { |
| base::HistogramTester histogram; |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| shared_info.debug_mode = AggregatableReportSharedInfo::DebugMode::kEnabled; |
| |
| std::optional<AggregatableReportRequest> standard_request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), shared_info.Clone(), |
| /*reporting_path=*/"/example-reporting-path"); |
| ASSERT_TRUE(standard_request.has_value()); |
| |
| PrivateAggregationBudgetKey protected_audience_key = |
| PrivateAggregationBudgetKey::Create( |
| example_request.shared_info().reporting_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| PrivateAggregationBudgetKey shared_storage_key = |
| PrivateAggregationBudgetKey::Create( |
| example_request.shared_info().reporting_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kSharedStorage) |
| .value(); |
| |
| Checkpoint checkpoint; |
| { |
| testing::InSequence seq; |
| |
| EXPECT_CALL(*budgeter_, ConsumeBudget(_, protected_audience_key, _)) |
| .WillOnce( |
| Invoke([](int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void( |
| PrivateAggregationBudgeter::RequestResult)> on_done) { |
| std::move(on_done).Run( |
| PrivateAggregationBudgeter::RequestResult::kApproved); |
| })); |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport) |
| .WillOnce(Invoke([&](AggregatableReportRequest report_request) { |
| EXPECT_EQ(report_request.shared_info().reporting_origin, |
| example_request.shared_info().reporting_origin); |
| EXPECT_EQ(report_request.reporting_path(), |
| "/.well-known/private-aggregation/debug/" |
| "report-protected-audience"); |
| })); |
| // Still triggers the standard (non-debug) report. |
| EXPECT_CALL(*aggregation_service_, ScheduleReport); |
| |
| EXPECT_CALL(checkpoint, Call(1)); |
| |
| EXPECT_CALL(*budgeter_, ConsumeBudget(_, shared_storage_key, _)) |
| .WillOnce( |
| Invoke([](int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void( |
| PrivateAggregationBudgeter::RequestResult)> on_done) { |
| std::move(on_done).Run( |
| PrivateAggregationBudgeter::RequestResult::kApproved); |
| })); |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport) |
| .WillOnce(Invoke([&](AggregatableReportRequest report_request) { |
| EXPECT_EQ(report_request.shared_info().reporting_origin, |
| example_request.shared_info().reporting_origin); |
| EXPECT_EQ( |
| report_request.reporting_path(), |
| "/.well-known/private-aggregation/debug/report-shared-storage"); |
| })); |
| // Still triggers the standard (non-debug) report. |
| EXPECT_CALL(*aggregation_service_, ScheduleReport); |
| } |
| |
| { |
| auto [generator, contributions] = |
| CloneAndSplitOutGenerator(standard_request.value()); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), protected_audience_key, |
| BudgetDeniedBehavior::kDontSendReport); |
| } |
| checkpoint.Call(1); |
| { |
| auto [generator, contributions] = |
| CloneAndSplitOutGenerator(standard_request.value()); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), shared_storage_key, |
| BudgetDeniedBehavior::kDontSendReport); |
| } |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kApproved, 2); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kSentWithContributions, 2); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| BudgetDeniedWithDontSendReportBehavior_DebugRequestNotAssembledOrSent) { |
| base::HistogramTester histogram; |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| shared_info.debug_mode = AggregatableReportSharedInfo::DebugMode::kEnabled; |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_request.shared_info().reporting_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| std::optional<AggregatableReportRequest> standard_request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), shared_info.Clone(), |
| /*reporting_path=*/"/example-reporting-path"); |
| ASSERT_TRUE(standard_request.has_value()); |
| |
| EXPECT_CALL( |
| *budgeter_, |
| ConsumeBudget(standard_request->payload_contents().contributions[0].value, |
| example_key, _)) |
| .WillOnce(Invoke( |
| [](int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void(PrivateAggregationBudgeter::RequestResult)> |
| on_done) { |
| std::move(on_done).Run( |
| PrivateAggregationBudgeter::RequestResult::kBadValuesOnDisk); |
| })); |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport).Times(0); |
| EXPECT_CALL(*aggregation_service_, ScheduleReport).Times(0); |
| |
| auto [generator, contributions] = |
| CloneAndSplitOutGenerator(standard_request.value()); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), example_key, |
| BudgetDeniedBehavior::kDontSendReport); |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kBadValuesOnDisk, 1); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kNotSent, 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| BudgetDeniedWithSendNullReportBehavior_RequestSent) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| kPrivateAggregationApiBundledEnhancements); |
| base::HistogramTester histogram; |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| shared_info.debug_mode = AggregatableReportSharedInfo::DebugMode::kEnabled; |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_request.shared_info().reporting_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| AggregationServicePayloadContents null_payload = |
| example_request.payload_contents(); |
| null_payload.contributions = { |
| blink::mojom::AggregatableReportHistogramContribution(/*bucket=*/0, |
| /*value=*/0)}; |
| |
| std::optional<AggregatableReportRequest> null_request = |
| AggregatableReportRequest::Create( |
| null_payload, shared_info.Clone(), |
| /*reporting_path=*/"/example-reporting-path"); |
| std::optional<AggregatableReportRequest> expected_null_debug_request = |
| AggregatableReportRequest::Create( |
| null_payload, std::move(shared_info), |
| /*reporting_path=*/ |
| "/.well-known/private-aggregation/debug/report-protected-audience"); |
| ASSERT_TRUE(null_request.has_value()); |
| ASSERT_TRUE(expected_null_debug_request.has_value()); |
| |
| EXPECT_CALL( |
| *budgeter_, |
| ConsumeBudget(example_request.payload_contents().contributions[0].value, |
| example_key, _)) |
| .WillOnce(Invoke( |
| [](int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void(PrivateAggregationBudgeter::RequestResult)> |
| on_done) { |
| std::move(on_done).Run(PrivateAggregationBudgeter::RequestResult:: |
| kInsufficientLargerScopeBudget); |
| })); |
| |
| // Triggers the debug report |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport) |
| .WillOnce(Invoke([&expected_null_debug_request]( |
| AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, expected_null_debug_request.value())); |
| })); |
| |
| // Triggers the standard (non-debug) report. |
| EXPECT_CALL(*aggregation_service_, ScheduleReport) |
| .WillOnce( |
| Invoke([&null_request](AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, null_request.value())); |
| })); |
| |
| auto [generator, null_contributions] = |
| CloneAndSplitOutGenerator(null_request.value()); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), example_request.payload_contents().contributions, |
| example_key, BudgetDeniedBehavior::kSendNullReport); |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kInsufficientLargerScopeBudget, |
| 1); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult:: |
| kSentButContributionsClearedDueToBudgetDenial, |
| 1); |
| } |
| |
| TEST_F( |
| PrivateAggregationManagerImplTest, |
| BudgetDeniedWithSendNullReportBehaviorButFeatureParamDisabled_RequestNotSent) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndDisableFeature( |
| kPrivateAggregationApiBundledEnhancements); |
| base::HistogramTester histogram; |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| shared_info.debug_mode = AggregatableReportSharedInfo::DebugMode::kEnabled; |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_request.shared_info().reporting_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| std::optional<AggregatableReportRequest> standard_request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), shared_info.Clone(), |
| /*reporting_path=*/"/example-reporting-path"); |
| ASSERT_TRUE(standard_request.has_value()); |
| |
| EXPECT_CALL( |
| *budgeter_, |
| ConsumeBudget(standard_request->payload_contents().contributions[0].value, |
| example_key, _)) |
| .WillOnce(Invoke( |
| [](int, const PrivateAggregationBudgetKey&, |
| base::OnceCallback<void(PrivateAggregationBudgeter::RequestResult)> |
| on_done) { |
| std::move(on_done).Run(PrivateAggregationBudgeter::RequestResult:: |
| kInsufficientLargerScopeBudget); |
| })); |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport).Times(0); |
| EXPECT_CALL(*aggregation_service_, ScheduleReport).Times(0); |
| |
| auto [generator, contributions] = |
| CloneAndSplitOutGenerator(standard_request.value()); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), std::move(contributions), example_key, |
| BudgetDeniedBehavior::kSendNullReport); |
| |
| histogram.ExpectUniqueSample( |
| kBudgeterResultHistogram, |
| PrivateAggregationBudgeter::RequestResult::kInsufficientLargerScopeBudget, |
| 1); |
| |
| // The disabled feature does not interfere with the histogram, only the report |
| // being sent. |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult:: |
| kSentButContributionsClearedDueToBudgetDenial, |
| 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| NoContributions_BudgetNotCheckedButNullReportSent) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| kPrivateAggregationApiBundledEnhancements); |
| base::HistogramTester histogram; |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| shared_info.debug_mode = AggregatableReportSharedInfo::DebugMode::kEnabled; |
| |
| PrivateAggregationBudgetKey example_key = |
| PrivateAggregationBudgetKey::Create( |
| example_request.shared_info().reporting_origin, kExampleTime, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience) |
| .value(); |
| |
| AggregationServicePayloadContents null_payload = |
| example_request.payload_contents(); |
| null_payload.contributions = { |
| blink::mojom::AggregatableReportHistogramContribution(/*bucket=*/0, |
| /*value=*/0)}; |
| |
| std::optional<AggregatableReportRequest> null_request = |
| AggregatableReportRequest::Create( |
| null_payload, shared_info.Clone(), |
| /*reporting_path=*/"/example-reporting-path"); |
| std::optional<AggregatableReportRequest> expected_null_debug_request = |
| AggregatableReportRequest::Create( |
| null_payload, std::move(shared_info), |
| /*reporting_path=*/ |
| "/.well-known/private-aggregation/debug/report-protected-audience"); |
| ASSERT_TRUE(null_request.has_value()); |
| ASSERT_TRUE(expected_null_debug_request.has_value()); |
| |
| EXPECT_CALL(*budgeter_, ConsumeBudget).Times(0); |
| |
| // Triggers the debug report |
| EXPECT_CALL(*aggregation_service_, AssembleAndSendReport) |
| .WillOnce(Invoke([&expected_null_debug_request]( |
| AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, expected_null_debug_request.value())); |
| })); |
| |
| // Triggers the standard (non-debug) report. |
| EXPECT_CALL(*aggregation_service_, ScheduleReport) |
| .WillOnce( |
| Invoke([&null_request](AggregatableReportRequest report_request) { |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| report_request, null_request.value())); |
| })); |
| |
| auto [generator, null_contributions] = |
| CloneAndSplitOutGenerator(null_request.value()); |
| manager_.OnReportRequestDetailsReceivedFromHost( |
| std::move(generator), /*contributions=*/{}, example_key, |
| BudgetDeniedBehavior::kSendNullReport); |
| |
| histogram.ExpectTotalCount(kBudgeterResultHistogram, 0); |
| histogram.ExpectUniqueSample( |
| kManagerResultHistogram, |
| PrivateAggregationManagerImpl::RequestResult::kSentWithoutContributions, |
| 1); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| BindNewReceiver_InvokesHostMethodIdentically) { |
| const url::Origin example_origin = |
| url::Origin::Create(GURL(kExampleOriginUrl)); |
| const url::Origin example_main_frame_origin = |
| url::Origin::Create(GURL(kExampleMainFrameUrl)); |
| const url::Origin example_coordinator_origin = |
| url::Origin::Create(GURL(kExampleCoordinatorUrl)); |
| |
| EXPECT_CALL(*host_, BindNewReceiver( |
| example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience, |
| testing::Eq(std::nullopt), testing::Eq(std::nullopt), |
| testing::Eq(std::nullopt), _)) |
| .WillOnce(Return(true)); |
| EXPECT_TRUE(manager_.BindNewReceiver( |
| example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience, |
| /*context_id=*/std::nullopt, /*timeout=*/std::nullopt, |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>())); |
| |
| EXPECT_CALL(*host_, BindNewReceiver( |
| example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kSharedStorage, |
| testing::Eq(std::nullopt), testing::Eq(std::nullopt), |
| testing::Eq(std::nullopt), _)) |
| .WillOnce(Return(false)); |
| EXPECT_FALSE(manager_.BindNewReceiver( |
| example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kSharedStorage, |
| /*context_id=*/std::nullopt, /*timeout=*/std::nullopt, |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>())); |
| |
| EXPECT_CALL( |
| *host_, |
| BindNewReceiver(example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience, |
| testing::Eq("example_context_id"), |
| testing::Eq(std::nullopt), testing::Eq(std::nullopt), _)) |
| .WillOnce(Return(true)); |
| EXPECT_TRUE(manager_.BindNewReceiver( |
| example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience, |
| "example_context_id", /*timeout=*/std::nullopt, |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>())); |
| |
| EXPECT_CALL(*host_, |
| BindNewReceiver(example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kSharedStorage, |
| testing::Eq("example_context_id"), |
| testing::Eq(base::Seconds(5)), |
| testing::Eq(std::nullopt), _)) |
| .WillOnce(Return(true)); |
| EXPECT_TRUE(manager_.BindNewReceiver( |
| example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kSharedStorage, "example_context_id", |
| /*timeout=*/base::Seconds(5), |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>())); |
| |
| EXPECT_CALL(*host_, BindNewReceiver( |
| example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience, |
| testing::Eq(std::nullopt), testing::Eq(std::nullopt), |
| testing::Eq(example_coordinator_origin), _)) |
| .WillOnce(Return(true)); |
| EXPECT_TRUE(manager_.BindNewReceiver( |
| example_origin, example_main_frame_origin, |
| PrivateAggregationBudgetKey::Api::kProtectedAudience, |
| /*context_id=*/std::nullopt, /*timeout=*/std::nullopt, |
| example_coordinator_origin, |
| mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>())); |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| ClearBudgetingData_InvokesClearDataIdentically) { |
| { |
| base::RunLoop run_loop; |
| EXPECT_CALL(*budgeter_, |
| ClearData(kExampleTime, kExampleTime + base::Days(1), _, _)) |
| .WillOnce(Invoke([](base::Time delete_begin, base::Time delete_end, |
| StoragePartition::StorageKeyMatcherFunction filter, |
| base::OnceClosure done) { |
| EXPECT_TRUE(filter.is_null()); |
| std::move(done).Run(); |
| })); |
| manager_.ClearBudgetData(kExampleTime, kExampleTime + base::Days(1), |
| StoragePartition::StorageKeyMatcherFunction(), |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| StoragePartition::StorageKeyMatcherFunction example_filter; |
| example_filter = |
| base::BindLambdaForTesting([](const blink::StorageKey& storage_key) { |
| return storage_key.origin() == |
| url::Origin::Create(GURL("https://example.com")); |
| }); |
| |
| { |
| base::RunLoop run_loop; |
| EXPECT_CALL(*budgeter_, |
| ClearData(kExampleTime - base::Days(10), kExampleTime, _, _)) |
| .WillOnce(Invoke([&example_filter]( |
| base::Time delete_begin, base::Time delete_end, |
| StoragePartition::StorageKeyMatcherFunction filter, |
| base::OnceClosure done) { |
| EXPECT_EQ(filter, example_filter); |
| std::move(done).Run(); |
| })); |
| manager_.ClearBudgetData(kExampleTime - base::Days(10), kExampleTime, |
| example_filter, run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| } |
| |
| TEST_F(PrivateAggregationManagerImplTest, |
| BrowsingDataModel_CallbacksProperlyCalled) { |
| AggregatableReportRequest expected_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| std::vector<PrivateAggregationDataModel::DataKey> expected = { |
| PrivateAggregationDataModel::DataKey( |
| url::Origin::Create(GURL("https://example.com"))), |
| PrivateAggregationDataModel::DataKey( |
| url::Origin::Create(GURL("https://example2.com")))}; |
| |
| { |
| base::RunLoop run_loop; |
| std::set<PrivateAggregationDataModel::DataKey> data_keys; |
| auto cb = base::BindLambdaForTesting( |
| [&](std::set<PrivateAggregationDataModel::DataKey> returned_keys) { |
| data_keys = std::move(returned_keys); |
| }); |
| |
| EXPECT_CALL(*budgeter_, GetAllDataKeys) |
| .WillOnce(base::test::RunOnceCallback<0>( |
| std::set<PrivateAggregationDataModel::DataKey>{expected[0]})); |
| EXPECT_CALL(*aggregation_service_, GetPendingReportReportingOrigins) |
| .WillOnce(testing::DoAll( |
| base::test::RunOnceClosure(run_loop.QuitClosure()), |
| base::test::RunOnceCallback<0>( |
| std::set<url::Origin>{expected[1].reporting_origin()}))); |
| |
| manager_.GetAllDataKeys(cb); |
| run_loop.Run(); |
| |
| EXPECT_THAT(data_keys, |
| testing::UnorderedElementsAre(expected[0], expected[1])); |
| } |
| |
| { |
| base::RunLoop run_loop; |
| |
| PrivateAggregationDataModel::DataKey data_key( |
| expected_request.shared_info().reporting_origin); |
| |
| EXPECT_CALL(*budgeter_, DeleteByDataKey) |
| .WillOnce(base::test::RunOnceCallback<1>()); |
| EXPECT_CALL(*aggregation_service_, ClearData) |
| .WillOnce(base::test::RunOnceCallback<3>()); |
| |
| manager_.RemovePendingDataKey(data_key, run_loop.QuitClosure()); |
| |
| run_loop.Run(); |
| } |
| } |
| |
| } // namespace content |