blob: 99d3bf0dd46adb04ba5fccde31eabfd1f2bdbd17 [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_host.h"
#include <stddef.h>
#include <stdint.h>
#include <array>
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/types/zip.h"
#include "base/values.h"
#include "components/aggregation_service/aggregation_coordinator_utils.h"
#include "content/browser/aggregation_service/aggregatable_report.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_caller_api.h"
#include "content/browser/private_aggregation/private_aggregation_features.h"
#include "content/browser/private_aggregation/private_aggregation_manager.h"
#include "content/browser/private_aggregation/private_aggregation_pending_contributions.h"
#include "content/browser/private_aggregation/private_aggregation_test_utils.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_utils.h"
#include "mojo/public/cpp/bindings/remote.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/features_generated.h"
#include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h"
#include "third_party/blink/public/mojom/private_aggregation/private_aggregation_host.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
using NullReportBehavior = PrivateAggregationHost::NullReportBehavior;
using testing::_;
using testing::Invoke;
using testing::Property;
auto GenerateAndSaveReportRequest(
std::optional<AggregatableReportRequest>* out) {
return [out](PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
auto&&...) {
AggregatableReportRequest request =
GenerateReportRequest(std::move(generator), std::move(contributions));
if (out) {
*out = std::move(request);
}
};
}
auto SaveGeneratorAndPendingContributions(
std::optional<PrivateAggregationHost::ReportRequestGenerator>*
out_generator,
std::optional<PrivateAggregationPendingContributions>* out_contributions) {
return [out_generator, out_contributions](
PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
auto&&...) {
ASSERT_TRUE(out_generator);
ASSERT_TRUE(out_contributions);
ASSERT_TRUE(base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting));
*out_generator = std::move(generator);
*out_contributions = std::move(contributions.GetPendingContributions());
};
}
constexpr std::string_view kPipeResultHistogram =
"PrivacySandbox.PrivateAggregation.Host.PipeResult";
constexpr std::string_view kTimeoutResultHistogram =
"PrivacySandbox.PrivateAggregation.Host.TimeoutResult";
constexpr std::string_view kTimeToGenerateReportRequestWithContextIdHistogram =
"PrivacySandbox.PrivateAggregation.Host."
"TimeToGenerateReportRequestWithContextId";
constexpr std::string_view kFilteringIdStatusHistogram =
"PrivacySandbox.PrivateAggregation.Host.FilteringIdStatus";
void ExpectHistogramValueWithSuffixes(const base::HistogramTester& tester,
std::string_view base_histogram,
size_t value,
PrivateAggregationCallerApi caller_api,
bool is_reduced_delay) {
tester.ExpectUniqueSample(base_histogram, value, /*expected_bucket_count=*/1);
tester.ExpectUniqueSample(
base::StrCat({base_histogram, ".ProtectedAudience"}), value,
/*expected_bucket_count=*/
(caller_api == PrivateAggregationCallerApi::kProtectedAudience) ? 1 : 0);
tester.ExpectUniqueSample(
base::StrCat({base_histogram, ".SharedStorage"}), value,
/*expected_bucket_count=*/
(caller_api == PrivateAggregationCallerApi::kSharedStorage) ? 1 : 0);
tester.ExpectUniqueSample(
base::StrCat({base_histogram, ".SharedStorage.ReducedDelay"}), value,
/*expected_bucket_count=*/
(caller_api == PrivateAggregationCallerApi::kSharedStorage &&
is_reduced_delay)
? 1
: 0);
tester.ExpectUniqueSample(
base::StrCat({base_histogram, ".SharedStorage.FullDelay"}), value,
/*expected_bucket_count=*/
(caller_api == PrivateAggregationCallerApi::kSharedStorage &&
!is_reduced_delay)
? 1
: 0);
}
void ExpectNumberOfContributionMergeKeysHistogram(
const base::HistogramTester& tester,
size_t value,
PrivateAggregationCallerApi caller_api,
bool is_reduced_delay) {
constexpr std::string_view kBaseHistogram =
"PrivacySandbox.PrivateAggregation.Host.NumContributionMergeKeysInPipe";
constexpr std::string_view kBaseHistogramWithErrorReportingFeature =
"PrivacySandbox.PrivateAggregation.NumContributionMergeKeys";
const std::string_view base_histogram =
base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting)
? kBaseHistogramWithErrorReportingFeature
: kBaseHistogram;
ExpectHistogramValueWithSuffixes(tester, base_histogram, value, caller_api,
is_reduced_delay);
}
void ExpectNumberOfFinalUnmergedContributionsHistogram(
const base::HistogramTester& tester,
size_t value,
PrivateAggregationCallerApi caller_api,
bool is_reduced_delay) {
if (!base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting)) {
return;
}
ExpectHistogramValueWithSuffixes(
tester, /*base_histogram=*/
"PrivacySandbox.PrivateAggregation.NumFinalUnmergedContributions", value,
caller_api, is_reduced_delay);
}
void ExpectTruncationResultHistogram(
const base::HistogramTester& tester,
PrivateAggregationPendingContributions::TruncationResult value) {
if (!base::FeatureList::IsEnabled(
blink::features::kPrivateAggregationApiErrorReporting)) {
return;
}
constexpr std::string_view kBaseHistogram =
"PrivacySandbox.PrivateAggregation.TruncationResult";
tester.ExpectUniqueSample(kBaseHistogram, value, /*expected_bucket_count=*/1);
// This histogram is not split by API.
constexpr std::array<std::string_view, 4> kUnexpectedSuffixes = {
".ProtectedAudience", ".SharedStorage", ".SharedStorage.FullDelay",
".SharedStorage.ReducedDelay"};
for (std::string_view unexpected_suffix : kUnexpectedSuffixes) {
tester.ExpectTotalCount(base::StrCat({kBaseHistogram, unexpected_suffix}),
0);
}
}
class PrivateAggregationHostTestBase : public testing::Test {
public:
PrivateAggregationHostTestBase() = default;
void SetUp() override {
host_ = std::make_unique<PrivateAggregationHost>(
/*on_report_request_received=*/mock_callback_.Get(),
/*browser_context=*/&test_browser_context_);
}
void TearDown() override { host_.reset(); }
protected:
base::MockRepeatingCallback<void(
PrivateAggregationHost::ReportRequestGenerator,
PrivateAggregationPendingContributions::Wrapper,
PrivateAggregationBudgetKey,
NullReportBehavior)>
mock_callback_;
std::unique_ptr<PrivateAggregationHost> host_;
BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
private:
TestBrowserContext test_browser_context_;
base::test::ScopedFeatureList scoped_feature_list_{
kPrivateAggregationApiDebugModeRequires3pcEligibility};
};
class PrivateAggregationHostTest : public PrivateAggregationHostTestBase,
public testing::WithParamInterface<bool> {
public:
bool GetErrorReportingEnabledParam() const { return GetParam(); }
void SetUp() override {
scoped_feature_list_.InitWithFeatureState(
blink::features::kPrivateAggregationApiErrorReporting,
GetErrorReportingEnabledParam());
PrivateAggregationHostTestBase::SetUp();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(,
PrivateAggregationHostTest,
testing::Bool(),
[](auto& info) {
return info.param ? "ErrorReportingEnabled"
: "ErrorReportingDisabled";
});
class PrivateAggregationHostErrorReportingEnabledTest
: public PrivateAggregationHostTestBase {
private:
base::test::ScopedFeatureList scoped_feature_list_{
blink::features::kPrivateAggregationApiErrorReporting};
};
TEST_P(PrivateAggregationHostTest,
ContributeToHistogram_ReportRequestHasCorrectMembers) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_,
Run(_, _,
Property(&PrivateAggregationBudgetKey::caller_api,
PrivateAggregationCallerApi::kProtectedAudience),
NullReportBehavior::kDontSendReport))
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
// Should not get a request until after the remote is disconnected.
remote.FlushForTesting();
EXPECT_TRUE(remote.is_connected());
EXPECT_FALSE(validated_request);
remote.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(validated_request);
// We only do some basic validation for the scheduled report time and report
// ID as they are not deterministic and will be copied to `expected_request`.
// We're using `MOCK_TIME` so we can be sure no time has advanced.
base::Time now = base::Time::Now();
EXPECT_GE(validated_request->shared_info().scheduled_report_time,
now + base::Minutes(10) +
PrivateAggregationHost::kTimeForLocalProcessing);
EXPECT_LE(
validated_request->shared_info().scheduled_report_time,
now + base::Hours(1) + PrivateAggregationHost::kTimeForLocalProcessing);
EXPECT_TRUE(validated_request->shared_info().report_id.is_valid());
// We only made one contribution, and padding would be added later on by
// `AggregatableReport::Provider::CreateFromRequestAndPublicKeys()`.
EXPECT_EQ(validated_request->payload_contents().contributions.size(), 1u);
std::optional<AggregatableReportRequest> expected_request =
AggregatableReportRequest::Create(
AggregationServicePayloadContents(
AggregationServicePayloadContents::Operation::kHistogram,
{blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/123, /*value=*/456,
/*filtering_id=*/std::nullopt)},
/*aggregation_coordinator_origin=*/std::nullopt,
/*max_contributions_allowed=*/20u,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes),
AggregatableReportSharedInfo(
validated_request->shared_info().scheduled_report_time,
validated_request->shared_info().report_id,
/*reporting_origin=*/kExampleOrigin,
AggregatableReportSharedInfo::DebugMode::kDisabled,
/*additional_fields=*/base::Value::Dict(),
/*api_version=*/"1.0",
/*api_identifier=*/"protected-audience"),
AggregatableReportRequest::DelayType::ScheduledWithFullDelay,
/*reporting_path=*/
"/.well-known/private-aggregation/report-protected-audience");
ASSERT_TRUE(expected_request);
EXPECT_TRUE(aggregation_service::ReportRequestsEqual(
validated_request.value(), expected_request.value()));
histogram.ExpectUniqueSample(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
1);
}
TEST_P(PrivateAggregationHostTest, ApiDiffers_RequestUpdatesCorrectly) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
const auto apis = std::to_array<const PrivateAggregationCallerApi>({
PrivateAggregationCallerApi::kProtectedAudience,
PrivateAggregationCallerApi::kSharedStorage,
});
std::vector<mojo::Remote<blink::mojom::PrivateAggregationHost>> remotes{
/*n=*/2};
std::vector<std::optional<AggregatableReportRequest>> validated_requests{
/*n=*/2};
for (int i = 0; i < 2; i++) {
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin, apis[i], /*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remotes[i].BindNewPipeAndPassReceiver()));
EXPECT_CALL(
mock_callback_,
Run(_, _, Property(&PrivateAggregationBudgetKey::caller_api, apis[i]),
NullReportBehavior::kDontSendReport))
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[i]));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remotes[i]->ContributeToHistogram(std::move(contributions));
remotes[i].FlushForTesting();
EXPECT_TRUE(remotes[i].is_connected());
ASSERT_FALSE(validated_requests[i]);
remotes[i].reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(validated_requests[i]);
}
EXPECT_EQ(validated_requests[0]->reporting_path(),
"/.well-known/private-aggregation/report-protected-audience");
EXPECT_EQ(validated_requests[1]->reporting_path(),
"/.well-known/private-aggregation/report-shared-storage");
EXPECT_EQ(validated_requests[0]->shared_info().api_identifier,
"protected-audience");
EXPECT_EQ(validated_requests[1]->shared_info().api_identifier,
"shared-storage");
histogram.ExpectUniqueSample(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
2);
}
TEST_P(PrivateAggregationHostTest, EnableDebugMode_ReflectedInReport) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
std::vector<blink::mojom::DebugModeDetailsPtr> debug_mode_details_args;
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New());
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, /*debug_key=*/nullptr));
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true,
/*debug_key=*/blink::mojom::DebugKey::New(/*value=*/1234u)));
std::vector<mojo::Remote<blink::mojom::PrivateAggregationHost>> remotes{
/*n=*/3};
std::vector<std::optional<AggregatableReportRequest>> validated_requests{
/*n=*/3};
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[0]))
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[1]))
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[2]));
for (int i = 0; i < 3; ++i) {
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remotes[i].BindNewPipeAndPassReceiver()));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remotes[i]->ContributeToHistogram(std::move(contributions));
if (debug_mode_details_args[i]->is_enabled) {
remotes[i]->EnableDebugMode(
std::move(debug_mode_details_args[i]->debug_key));
}
remotes[i].reset();
}
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(validated_requests[0].has_value());
ASSERT_TRUE(validated_requests[1].has_value());
ASSERT_TRUE(validated_requests[2].has_value());
EXPECT_EQ(validated_requests[0]->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
EXPECT_EQ(validated_requests[1]->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
EXPECT_EQ(validated_requests[2]->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
EXPECT_EQ(validated_requests[0]->debug_key(), std::nullopt);
EXPECT_EQ(validated_requests[1]->debug_key(), std::nullopt);
EXPECT_EQ(validated_requests[2]->debug_key(), 1234u);
histogram.ExpectUniqueSample(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
3);
}
TEST_P(PrivateAggregationHostTest,
MultipleReceievers_ContributeToHistogramCallsRoutedCorrectly) {
base::HistogramTester histogram;
const url::Origin kExampleOriginA =
url::Origin::Create(GURL("https://a.example"));
const url::Origin kExampleOriginB =
url::Origin::Create(GURL("https://b.example"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
std::vector<mojo::Remote<blink::mojom::PrivateAggregationHost>> remotes(
/*n=*/4);
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOriginA, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remotes[0].BindNewPipeAndPassReceiver()));
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOriginB, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remotes[1].BindNewPipeAndPassReceiver()));
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOriginA, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage,
/*context_id=*/std::nullopt, /*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remotes[2].BindNewPipeAndPassReceiver()));
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOriginB, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage,
/*context_id=*/std::nullopt, /*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remotes[3].BindNewPipeAndPassReceiver()));
// Use the bucket as a sentinel to ensure that calls were routed correctly.
EXPECT_CALL(mock_callback_,
Run(_, _,
Property(&PrivateAggregationBudgetKey::caller_api,
PrivateAggregationCallerApi::kProtectedAudience),
NullReportBehavior::kDontSendReport))
.WillOnce(Invoke(
[&kExampleOriginB](
PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior) {
AggregatableReportRequest request = GenerateReportRequest(
std::move(generator), std::move(contributions));
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(budget_key.origin(), kExampleOriginB);
EXPECT_EQ(request.shared_info().reporting_origin, kExampleOriginB);
}));
EXPECT_CALL(mock_callback_,
Run(_, _,
Property(&PrivateAggregationBudgetKey::caller_api,
PrivateAggregationCallerApi::kSharedStorage),
NullReportBehavior::kDontSendReport))
.WillOnce(Invoke(
[&kExampleOriginA](
PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior) {
AggregatableReportRequest request = GenerateReportRequest(
std::move(generator), std::move(contributions));
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 2);
EXPECT_EQ(request.shared_info().reporting_origin, kExampleOriginA);
EXPECT_EQ(budget_key.origin(), kExampleOriginA);
}));
{
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/1, /*value=*/123, /*filtering_id=*/std::nullopt));
remotes[1]->ContributeToHistogram(std::move(contributions));
}
{
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/2, /*value=*/123, /*filtering_id=*/std::nullopt));
remotes[2]->ContributeToHistogram(std::move(contributions));
}
for (auto& remote : remotes) {
remote.reset();
}
host_->FlushReceiverSetForTesting();
histogram.ExpectTotalCount(kPipeResultHistogram, 4);
histogram.ExpectBucketCount(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
2);
histogram.ExpectBucketCount(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kNoReportButNoError, 2);
}
TEST_P(PrivateAggregationHostTest, BindUntrustworthyOriginReceiver_Fails) {
base::HistogramTester histogram;
const url::Origin kInsecureOrigin =
url::Origin::Create(GURL("http://example.com"));
const url::Origin kOpaqueOrigin;
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote_1;
EXPECT_FALSE(host_->BindNewReceiver(
kInsecureOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote_1.BindNewPipeAndPassReceiver()));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote_2;
EXPECT_FALSE(host_->BindNewReceiver(
kOpaqueOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote_2.BindNewPipeAndPassReceiver()));
// Attempt to send a message to an unconnected remote. The request should
// not be processed.
EXPECT_CALL(mock_callback_, Run).Times(0);
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote_1->ContributeToHistogram(std::move(contributions));
// Reset then flush to ensure disconnection and the ContributeToHistogram call
// have had time to be processed.
remote_1.reset();
remote_2.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectTotalCount(kPipeResultHistogram, 0);
}
TEST_P(PrivateAggregationHostTest, BindReceiverWithTooLongContextId_Fails) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
const std::string kTooLongContextId =
"this_is_an_example_of_a_context_id_that_is_too_long_to_be_allowed";
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_FALSE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience, kTooLongContextId,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
// Attempt to send a message to an unconnected remote. The request should
// not be processed.
EXPECT_CALL(mock_callback_, Run).Times(0);
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
// Reset then flush to ensure disconnection and the ContributeToHistogram call
// have had time to be processed.
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectTotalCount(kPipeResultHistogram, 0);
}
// Test that `BindNewReceiver()` fails when it's given `max_contributions`
// despite the controlling feature being disabled.
TEST_P(PrivateAggregationHostTest,
BindReceiverWithDisabledMaxContributions_Fails) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
blink::features::kPrivateAggregationApiMaxContributions);
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_FALSE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/123, remote.BindNewPipeAndPassReceiver()));
// Attempt to send a message to an unconnected remote. The request should
// not be processed.
EXPECT_CALL(mock_callback_, Run).Times(0);
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
// Reset then flush to ensure disconnection and the ContributeToHistogram call
// have had time to be processed.
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectTotalCount(kPipeResultHistogram, 0);
}
TEST_P(PrivateAggregationHostTest, TimeoutXorDeterministicReport_Fails) {
constexpr size_t kNonDefaultFilteringIdMaxBytes = 3;
static_assert(kNonDefaultFilteringIdMaxBytes !=
PrivateAggregationHost::kDefaultFilteringIdMaxBytes);
constexpr size_t kNonDefaultMaxContributions = 150;
EXPECT_NE(kNonDefaultMaxContributions,
PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kProtectedAudience,
/*requested_max_contributions=*/std::nullopt));
EXPECT_NE(kNonDefaultMaxContributions,
PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kSharedStorage,
/*requested_max_contributions=*/std::nullopt));
const struct {
std::string description;
std::optional<base::TimeDelta> timeout;
std::optional<std::string> context_id;
size_t filtering_id_max_bytes =
PrivateAggregationHost::kDefaultFilteringIdMaxBytes;
std::optional<size_t> max_contributions;
bool expect_should_send_deterministically = false;
} kTestCases[] = {
{
.description = "Timeout set, but should not send deterministically",
.timeout = base::Seconds(1),
.expect_should_send_deterministically = false,
},
{
.description = "Timeout not set, but should send deterministically "
"due to contextId",
.timeout = std::nullopt,
.context_id = "example_context_id",
.expect_should_send_deterministically = true,
},
{
.description = "Timeout not set, but should send deterministically "
"due to filteringIdMaxBytes",
.timeout = std::nullopt,
.filtering_id_max_bytes = kNonDefaultFilteringIdMaxBytes,
.expect_should_send_deterministically = true,
},
{
.description = "Timeout not set, but should send deterministically "
"due to maxContributions",
.timeout = std::nullopt,
.max_contributions = kNonDefaultMaxContributions,
.expect_should_send_deterministically = true,
},
};
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
for (bool enable_max_contributions_feature : {false, true}) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatureState(
blink::features::kPrivateAggregationApiMaxContributions,
enable_max_contributions_feature);
for (const auto& test_case : kTestCases) {
for (const auto api : {PrivateAggregationCallerApi::kProtectedAudience,
PrivateAggregationCallerApi::kSharedStorage}) {
SCOPED_TRACE(testing::Message()
<< test_case.description
<< " [enable_max_contributions_feature="
<< enable_max_contributions_feature
<< ", api=" << testing::PrintToString(api) << "]");
EXPECT_EQ(
PrivateAggregationManager::ShouldSendReportDeterministically(
api, test_case.context_id, test_case.filtering_id_max_bytes,
test_case.max_contributions),
test_case.expect_should_send_deterministically);
// `BindNewReceiver()` requires that the `timeout` is set iff the report
// should be sent deterministically. We've demonstrated the inverse:
// that the `timeout` is set XOR the report should be sent
// deterministically. Therefore, binding the receiver should fail.
//
// N.B. While `kPrivateAggregationApiMaxContributions` exists, this
// expectation may subtly pass for a different reason -- because
// `max_contributions` was specified while its feature was disabled.
// This is why we repeat the test case with the feature enabled and
// disabled.
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_FALSE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin, api, test_case.context_id,
test_case.timeout,
/*aggregation_coordinator_origin=*/std::nullopt,
test_case.filtering_id_max_bytes, test_case.max_contributions,
remote.BindNewPipeAndPassReceiver()));
}
}
}
}
TEST_P(PrivateAggregationHostTest, TimeoutSetWithContextId_Succeeds) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/"example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
}
TEST_P(PrivateAggregationHostTest,
TimeoutSetWithNonDefaultFilteringIdMaxBytes_Succeeds) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
/*filtering_id_max_bytes=*/3,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
}
TEST_P(PrivateAggregationHostTest,
TimeoutSetWithNonDefaultMaxContributions_Succeeds) {
base::test::ScopedFeatureList scoped_feature_list(
blink::features::kPrivateAggregationApiMaxContributions);
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
const size_t default_max_contributions =
PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kSharedStorage, std::nullopt);
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage,
/*context_id=*/std::nullopt,
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
/*filtering_id_max_bytes=*/
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/default_max_contributions + 1,
remote.BindNewPipeAndPassReceiver()));
}
TEST_P(PrivateAggregationHostTest,
TimeoutSetWithDefaultMaxContributions_Fails) {
base::test::ScopedFeatureList scoped_feature_list(
blink::features::kPrivateAggregationApiMaxContributions);
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
// Select a value for `max_contributions` that is equivalent to
// `std::nullopt`.
const size_t default_max_contributions =
PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kSharedStorage,
/*requested_max_contributions=*/std::nullopt);
{
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_FALSE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage,
/*context_id=*/std::nullopt,
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
/*filtering_id_max_bytes=*/
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/default_max_contributions,
remote.BindNewPipeAndPassReceiver()));
}
{
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
/*filtering_id_max_bytes=*/
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/default_max_contributions,
remote.BindNewPipeAndPassReceiver()));
}
}
TEST_P(PrivateAggregationHostTest, InvalidRequest_Rejected) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
// Negative values are invalid
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
negative_contributions;
negative_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/-1, /*filtering_id=*/std::nullopt));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
valid_contributions;
valid_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
EXPECT_CALL(mock_callback_, Run).Times(0);
{
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
base::HistogramTester histogram;
remote->ContributeToHistogram(std::move(negative_contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kNegativeValue, 1);
}
{
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
base::HistogramTester histogram;
remote->ContributeToHistogram(std::move(valid_contributions));
remote->EnableDebugMode(
/*debug_key=*/nullptr);
remote->EnableDebugMode(
/*debug_key=*/blink::mojom::DebugKey::New(1234u));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kEnableDebugModeCalledMultipleTimes,
1);
}
}
TEST_F(PrivateAggregationHostErrorReportingEnabledTest,
InvalidOnEventCall_Rejected) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
// Negative values are invalid
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
negative_contributions;
negative_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/-1, /*filtering_id=*/std::nullopt));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
valid_contributions;
valid_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
EXPECT_CALL(mock_callback_, Run).Times(0);
{
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
base::HistogramTester histogram;
remote->ContributeToHistogramOnEvent(
blink::mojom::PrivateAggregationErrorEvent::kContributionTimeoutReached,
std::move(negative_contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kNegativeValue, 1);
}
{
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
base::HistogramTester histogram;
remote->ContributeToHistogramOnEvent(
blink::mojom::PrivateAggregationErrorEvent::kContributionTimeoutReached,
std::move(valid_contributions));
remote->EnableDebugMode(
/*debug_key=*/nullptr);
remote->EnableDebugMode(
/*debug_key=*/blink::mojom::DebugKey::New(1234u));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kEnableDebugModeCalledMultipleTimes,
1);
}
}
constexpr struct {
std::string_view label;
bool should_enable_per_context_sizing;
PrivateAggregationCallerApi caller_api;
std::optional<size_t> requested_max_contributions;
std::optional<base::TimeDelta> timeout;
size_t expected_num_contributions;
} kMaxNumContributionsTestCases[]{
// Simulate callers that omit `maxContributions` when per-context sizing is
// enabled.
{
"Shared Storage gets default number of contributions when per-context "
"sizing is enabled",
/*should_enable_per_context_sizing=*/true,
/*caller_api=*/PrivateAggregationCallerApi::kSharedStorage,
/*requested_max_contributions=*/std::nullopt,
/*timeout=*/std::nullopt,
/*expected_num_contributions=*/20,
},
{
"Protected Audience gets default number of contributions when "
"per-context sizing is enabled",
/*should_enable_per_context_sizing=*/true,
/*caller_api=*/PrivateAggregationCallerApi::kProtectedAudience,
/*requested_max_contributions=*/std::nullopt,
/*timeout=*/std::nullopt,
/*expected_num_contributions=*/100,
},
// Simulate callers that set `maxContributions` when per-context sizing is
// enabled.
{
"Shared Storage gets 10 contributions upon request",
/*should_enable_per_context_sizing=*/true,
/*caller_api=*/PrivateAggregationCallerApi::kSharedStorage,
/*requested_max_contributions=*/10,
/*timeout=*/base::Seconds(1),
/*expected_num_contributions=*/10,
},
{
"Protected Audience gets 10 contributions upon request",
/*should_enable_per_context_sizing=*/true,
/*caller_api=*/PrivateAggregationCallerApi::kProtectedAudience,
/*requested_max_contributions=*/10,
/*timeout=*/base::Seconds(1),
/*expected_num_contributions=*/10,
},
{
"Shared Storage gets 1000 contributions upon request",
/*should_enable_per_context_sizing=*/true,
/*caller_api=*/PrivateAggregationCallerApi::kSharedStorage,
/*requested_max_contributions=*/1000,
/*timeout=*/base::Seconds(1),
/*expected_num_contributions=*/1000,
},
{
"Protected Audience gets 1000 contributions upon request",
/*should_enable_per_context_sizing=*/true,
/*caller_api=*/PrivateAggregationCallerApi::kProtectedAudience,
/*requested_max_contributions=*/1000,
/*timeout=*/base::Seconds(1),
/*expected_num_contributions=*/1000,
},
{
"Shared Storage gets no more than 1000 contributions",
/*should_enable_per_context_sizing=*/true,
/*caller_api=*/PrivateAggregationCallerApi::kSharedStorage,
/*requested_max_contributions=*/1001,
/*timeout=*/base::Seconds(1),
/*expected_num_contributions=*/1000,
},
{
"Protected Audience gets no more than 1000 contributions",
/*should_enable_per_context_sizing=*/true,
/*caller_api=*/PrivateAggregationCallerApi::kProtectedAudience,
/*requested_max_contributions=*/1001,
/*timeout=*/base::Seconds(1),
/*expected_num_contributions=*/1000,
},
};
TEST_P(PrivateAggregationHostTest, TooManyContributions_Truncated) {
for (const auto& test_case : kMaxNumContributionsTestCases) {
SCOPED_TRACE(testing::Message() << test_case.label);
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatureState(
blink::features::kPrivateAggregationApiMaxContributions,
test_case.should_enable_per_context_sizing);
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin, test_case.caller_api,
/*context_id=*/std::nullopt,
/*timeout=*/test_case.timeout,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/test_case.requested_max_contributions,
remote.BindNewPipeAndPassReceiver()));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
too_many_contributions;
for (size_t i = 0; i < test_case.expected_num_contributions + 1; ++i) {
too_many_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123 + i, /*value=*/1, /*filtering_id=*/std::nullopt));
}
base::HistogramTester histogram;
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
remote->ContributeToHistogram(std::move(too_many_contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
GetErrorReportingEnabledParam()
? PrivateAggregationHost::PipeResult::kReportSuccess
: PrivateAggregationHost::PipeResult::
kReportSuccessButTruncatedDueToTooManyContributions,
1);
ExpectTruncationResultHistogram(
histogram, PrivateAggregationPendingContributions::TruncationResult::
kTruncationDueToUnconditionalContributions);
ASSERT_TRUE(validated_request);
EXPECT_EQ(validated_request->payload_contents().contributions.size(),
test_case.expected_num_contributions);
}
}
TEST_P(PrivateAggregationHostTest,
ContributionsMergedIffSameBucketAndFilteringId) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/1, /*filtering_id=*/std::nullopt));
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/0, /*filtering_id=*/std::nullopt));
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/1, /*filtering_id=*/0));
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/2, /*filtering_id=*/0));
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/1, /*filtering_id=*/1));
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/124, /*value=*/1, /*filtering_id=*/std::nullopt));
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/125, /*value=*/0, /*filtering_id=*/1));
base::HistogramTester histogram;
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
remote->ContributeToHistogram(std::move(contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
1);
ASSERT_TRUE(validated_request);
EXPECT_THAT(
validated_request->payload_contents().contributions,
testing::UnorderedElementsAre(
testing::Eq(blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/123, /*value=*/4, /*filtering_id=*/std::nullopt)),
testing::Eq(blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/123, /*value=*/1, /*filtering_id=*/1)),
testing::Eq(blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/124, /*value=*/1, /*filtering_id=*/std::nullopt))));
ExpectNumberOfContributionMergeKeysHistogram(
histogram, 3, PrivateAggregationCallerApi::kProtectedAudience,
/*is_reduced_delay=*/false);
ExpectNumberOfFinalUnmergedContributionsHistogram(
histogram, 5, PrivateAggregationCallerApi::kProtectedAudience,
/*is_reduced_delay=*/false);
ExpectTruncationResultHistogram(
histogram,
PrivateAggregationPendingContributions::TruncationResult::kNoTruncation);
}
TEST_P(PrivateAggregationHostTest,
MergeableContributions_NotTruncatedUnnecessarily) {
for (const auto& test_case : kMaxNumContributionsTestCases) {
SCOPED_TRACE(testing::Message() << test_case.label);
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatureState(
blink::features::kPrivateAggregationApiMaxContributions,
test_case.should_enable_per_context_sizing);
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin, test_case.caller_api,
/*context_id=*/std::nullopt,
/*timeout=*/test_case.timeout,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/test_case.requested_max_contributions,
remote.BindNewPipeAndPassReceiver()));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
too_many_contributions_unless_merged;
for (size_t i = 0; i < test_case.expected_num_contributions + 1; ++i) {
too_many_contributions_unless_merged.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/1, /*filtering_id=*/std::nullopt));
}
base::HistogramTester histogram;
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
remote->ContributeToHistogram(
std::move(too_many_contributions_unless_merged));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kReportSuccess, 1);
ASSERT_TRUE(validated_request);
ASSERT_EQ(validated_request->payload_contents().contributions.size(), 1u);
EXPECT_EQ(validated_request->payload_contents().contributions.at(0).bucket,
123);
EXPECT_EQ(
base::checked_cast<size_t>(
validated_request->payload_contents().contributions.at(0).value),
test_case.expected_num_contributions + 1);
EXPECT_EQ(
validated_request->payload_contents().contributions.at(0).filtering_id,
std::nullopt);
ExpectNumberOfContributionMergeKeysHistogram(
histogram, 1, test_case.caller_api,
/*is_reduced_delay=*/test_case.timeout.has_value());
ExpectNumberOfFinalUnmergedContributionsHistogram(
histogram, test_case.expected_num_contributions + 1,
test_case.caller_api,
/*is_reduced_delay=*/test_case.timeout.has_value());
ExpectTruncationResultHistogram(histogram,
PrivateAggregationPendingContributions::
TruncationResult::kNoTruncation);
}
}
TEST_P(PrivateAggregationHostTest,
ZeroValueContributions_DroppedAndTruncationHistogramNotTriggered) {
for (const auto& test_case : kMaxNumContributionsTestCases) {
SCOPED_TRACE(testing::Message() << test_case.label);
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatureState(
blink::features::kPrivateAggregationApiMaxContributions,
test_case.should_enable_per_context_sizing);
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin, test_case.caller_api,
/*context_id=*/std::nullopt,
/*timeout=*/test_case.timeout,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/test_case.requested_max_contributions,
remote.BindNewPipeAndPassReceiver()));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
for (size_t i = 0; i < test_case.expected_num_contributions; ++i) {
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123 + i, /*value=*/1, /*filtering_id=*/std::nullopt));
}
// This contribution would put us over the limit, but it will be dropped due
// to its zero value.
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123 + test_case.expected_num_contributions,
/*value=*/0, /*filtering_id=*/std::nullopt));
base::HistogramTester histogram;
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
remote->ContributeToHistogram(std::move(contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
// Histogram does *not* indicate truncation as only zero value contributions
// were lost.
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kReportSuccess, 1);
ASSERT_TRUE(validated_request);
EXPECT_EQ(validated_request->payload_contents().contributions.size(),
test_case.expected_num_contributions);
ExpectNumberOfContributionMergeKeysHistogram(
histogram, test_case.expected_num_contributions, test_case.caller_api,
/*is_reduced_delay=*/test_case.timeout.has_value());
ExpectNumberOfFinalUnmergedContributionsHistogram(
histogram, test_case.expected_num_contributions, test_case.caller_api,
/*is_reduced_delay=*/test_case.timeout.has_value());
ExpectTruncationResultHistogram(histogram,
PrivateAggregationPendingContributions::
TruncationResult::kNoTruncation);
}
}
TEST_P(PrivateAggregationHostTest,
NumberOfContributionMergeKeysHistograms_RecordsCorrectSubMetrics) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
example_contributions;
example_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/1, /*filtering_id=*/std::nullopt));
example_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/1, /*filtering_id=*/0));
example_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/2, /*filtering_id=*/0));
example_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/1, /*filtering_id=*/1));
example_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/124, /*value=*/1, /*filtering_id=*/std::nullopt));
constexpr size_t kExpectedNumberMergeKeys = 3;
constexpr size_t kExpectedNumFinalUnmergedContributions = 5;
const struct {
const std::string_view description;
PrivateAggregationCallerApi caller_api;
std::optional<std::string> context_id;
size_t filtering_id_max_bytes;
std::optional<base::TimeDelta> timeout;
} kTestCases[] = {
{
"Protected Audience",
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*timeout=*/std::nullopt,
},
{
"Shared Storage full delay",
PrivateAggregationCallerApi::kSharedStorage,
/*context_id=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*timeout=*/std::nullopt,
},
{
"Shared Storage reduced delay due to context ID",
PrivateAggregationCallerApi::kSharedStorage,
/*context_id=*/"example_context_id",
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*timeout=*/base::Seconds(5),
},
{
"Shared Storage reduced delay due to filtering ID max bytes",
PrivateAggregationCallerApi::kSharedStorage,
/*context_id=*/std::nullopt,
/*filtering_id_max_bytes=*/3,
/*timeout=*/base::Seconds(5),
},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.description);
base::HistogramTester histogram;
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
bool bind_result = host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin, test_case.caller_api,
/*context_id=*/test_case.context_id, /*timeout=*/test_case.timeout,
/*aggregation_coordinator_origin=*/std::nullopt,
/*filtering_id_max_bytes=*/test_case.filtering_id_max_bytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver());
EXPECT_TRUE(bind_result);
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
for (const auto& contribution : example_contributions) {
contributions.push_back(contribution->Clone());
}
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(nullptr));
remote->ContributeToHistogram(std::move(contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
ExpectNumberOfContributionMergeKeysHistogram(
histogram, kExpectedNumberMergeKeys, test_case.caller_api,
/*is_reduced_delay=*/test_case.timeout.has_value());
ExpectNumberOfFinalUnmergedContributionsHistogram(
histogram, kExpectedNumFinalUnmergedContributions, test_case.caller_api,
/*is_reduced_delay=*/test_case.timeout.has_value());
ExpectTruncationResultHistogram(histogram,
PrivateAggregationPendingContributions::
TruncationResult::kNoTruncation);
}
}
TEST_P(PrivateAggregationHostTest, PrivateAggregationAllowed_RequestSucceeds) {
base::HistogramTester histogram;
MockPrivateAggregationContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
// If the API is enabled, the call should succeed.
EXPECT_CALL(browser_client, IsPrivateAggregationAllowed(_, kMainFrameOrigin,
kExampleOrigin, _))
.Times(2)
.WillRepeatedly(testing::Return(true));
EXPECT_CALL(mock_callback_, Run);
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
1);
}
TEST_P(PrivateAggregationHostTest, PrivateAggregationDisallowed_RequestFails) {
base::HistogramTester histogram;
MockPrivateAggregationContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
// If the API is enabled, the call should succeed.
EXPECT_CALL(browser_client, IsPrivateAggregationAllowed(_, kMainFrameOrigin,
kExampleOrigin, _))
.WillOnce(testing::Return(false));
EXPECT_CALL(mock_callback_, Run).Times(0);
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kApiDisabledInSettings, 1);
}
TEST_P(PrivateAggregationHostTest, ContextIdSet_ReflectedInSingleReport) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience, "example_context_id",
/*timeout=*/base::Seconds(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
constexpr base::TimeDelta kTimeToGenerateReportRequest =
base::Milliseconds(123);
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_, Run(_, _, _, NullReportBehavior::kSendNullReport))
.WillOnce(testing::DoAll(
[&] {
task_environment_.FastForwardBy(kTimeToGenerateReportRequest);
},
GenerateAndSaveReportRequest(&validated_request)));
{
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
}
remote.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(validated_request.has_value());
EXPECT_THAT(
validated_request->additional_fields(),
testing::ElementsAre(testing::Pair("context_id", "example_context_id")));
histogram.ExpectUniqueSample(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
1);
histogram.ExpectUniqueTimeSample(
kTimeToGenerateReportRequestWithContextIdHistogram,
kTimeToGenerateReportRequest, 1);
}
TEST_P(
PrivateAggregationHostTest,
ContextIdSetNoContributions_NullReportSentWithDebugModeDependentOnFeature) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
std::vector<blink::mojom::DebugModeDetailsPtr> debug_mode_details_args;
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New());
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, /*debug_key=*/nullptr));
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true,
/*debug_key=*/blink::mojom::DebugKey::New(/*value=*/1234u)));
std::vector<std::optional<AggregatableReportRequest>> validated_requests{
/*n=*/3};
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[0]))
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[1]))
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[2]));
for (auto& debug_mode_details_arg : debug_mode_details_args) {
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience, "example_context_id",
/*timeout=*/base::Seconds(5),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
if (debug_mode_details_arg->is_enabled) {
remote->EnableDebugMode(
std::move(debug_mode_details_arg->Clone()->debug_key));
}
EXPECT_TRUE(remote.is_connected());
remote.reset();
}
host_->FlushReceiverSetForTesting();
ASSERT_EQ(validated_requests.size(), debug_mode_details_args.size());
for (auto [validated_request, debug_mode_details_arg] :
base::zip(validated_requests, debug_mode_details_args)) {
ASSERT_TRUE(validated_request.has_value());
EXPECT_THAT(validated_request->additional_fields(),
testing::ElementsAre(
testing::Pair("context_id", "example_context_id")));
ASSERT_TRUE(validated_request->payload_contents().contributions.empty());
// Null reports have debug mode set only if the error reporting feature is
// enabled.
if (GetErrorReportingEnabledParam()) {
EXPECT_EQ(validated_request->shared_info().debug_mode,
debug_mode_details_arg->is_enabled
? AggregatableReportSharedInfo::DebugMode::kEnabled
: AggregatableReportSharedInfo::DebugMode::kDisabled);
std::optional<uint64_t> expected_debug_key;
if (debug_mode_details_arg->debug_key) {
expected_debug_key = debug_mode_details_arg->debug_key->value;
}
EXPECT_EQ(validated_request->debug_key(), expected_debug_key);
} else {
EXPECT_EQ(validated_request->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
EXPECT_EQ(validated_request->debug_key(), std::nullopt);
}
}
}
TEST_P(
PrivateAggregationHostTest,
FilteringIdMaxBytesSetNoContributions_NullReportSentWithDebugModeDependentOnFeature) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
std::vector<blink::mojom::DebugModeDetailsPtr> debug_mode_details_args;
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New());
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, /*debug_key=*/nullptr));
debug_mode_details_args.push_back(blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true,
/*debug_key=*/blink::mojom::DebugKey::New(/*value=*/1234u)));
std::vector<std::optional<AggregatableReportRequest>> validated_requests{
/*n=*/3};
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[0]))
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[1]))
.WillOnce(GenerateAndSaveReportRequest(&validated_requests[2]));
for (auto& debug_mode_details_arg : debug_mode_details_args) {
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(
host_->BindNewReceiver(kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/base::Seconds(1),
/*aggregation_coordinator_origin=*/std::nullopt,
/*filtering_id_max_bytes=*/3u,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
if (debug_mode_details_arg->is_enabled) {
remote->EnableDebugMode(
std::move(debug_mode_details_arg->Clone()->debug_key));
}
EXPECT_TRUE(remote.is_connected());
remote.reset();
}
host_->FlushReceiverSetForTesting();
ASSERT_EQ(validated_requests.size(), debug_mode_details_args.size());
for (size_t i = 0; i < validated_requests.size(); i++) {
std::optional<AggregatableReportRequest>& validated_request =
validated_requests[i];
ASSERT_TRUE(validated_request.has_value());
EXPECT_THAT(validated_request->additional_fields(), testing::IsEmpty());
ASSERT_TRUE(validated_request->payload_contents().contributions.empty());
// Null reports have debug mode set only if the error reporting feature is
// enabled.
if (GetErrorReportingEnabledParam()) {
blink::mojom::DebugModeDetailsPtr& debug_mode_details_arg =
debug_mode_details_args[i];
EXPECT_EQ(validated_request->shared_info().debug_mode,
debug_mode_details_arg->is_enabled
? AggregatableReportSharedInfo::DebugMode::kEnabled
: AggregatableReportSharedInfo::DebugMode::kDisabled);
std::optional<uint64_t> expected_debug_key;
if (debug_mode_details_arg->debug_key) {
expected_debug_key = debug_mode_details_arg->debug_key->value;
}
EXPECT_EQ(validated_request->debug_key(), expected_debug_key);
} else {
EXPECT_EQ(validated_request->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
EXPECT_EQ(validated_request->debug_key(), std::nullopt);
}
}
}
TEST_P(PrivateAggregationHostTest,
NeitherContextIdNorFilteringIdMaxBytesSet_NoNullReportSent) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
EXPECT_CALL(mock_callback_, Run).Times(0);
{
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
EXPECT_TRUE(remote.is_connected());
remote.reset();
host_->FlushReceiverSetForTesting();
}
{
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
// Enabling the debug mode has no effect.
remote->EnableDebugMode(
/*debug_key=*/blink::mojom::DebugKey::New(/*value=*/1234u));
EXPECT_TRUE(remote.is_connected());
remote.reset();
host_->FlushReceiverSetForTesting();
}
// This histogram should only be recorded when there is a context ID.
histogram.ExpectTotalCount(kTimeToGenerateReportRequestWithContextIdHistogram,
0);
}
TEST_P(PrivateAggregationHostTest, AggregationCoordinatorOrigin) {
::aggregation_service::ScopedAggregationCoordinatorAllowlistForTesting
scoped_coordinator_allowlist(
{url::Origin::Create(GURL("https://a.test"))});
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
const url::Origin kValidCoordinatorOrigin =
url::Origin::Create(GURL("https://a.test"));
const url::Origin kInvalidCoordinatorOrigin =
url::Origin::Create(GURL("https://b.test"));
const struct {
const char* description;
const std::optional<url::Origin> aggregation_coordinator_origin;
bool expected_bind_result;
} kTestCases[] = {
{
"aggregation_coordinator_origin_empty",
std::nullopt,
true,
},
{
"aggregation_coordinator_origin_valid",
kValidCoordinatorOrigin,
true,
},
{
"aggregation_coordinator_origin_invalid",
kInvalidCoordinatorOrigin,
false,
},
};
for (const auto& test_case : kTestCases) {
base::HistogramTester histogram;
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
bool bind_result = host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt, /*timeout=*/std::nullopt,
test_case.aggregation_coordinator_origin,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver());
EXPECT_EQ(bind_result, test_case.expected_bind_result);
if (!bind_result) {
continue;
}
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kReportSuccess, 1);
ASSERT_TRUE(validated_request);
EXPECT_EQ(
validated_request->payload_contents().aggregation_coordinator_origin,
test_case.aggregation_coordinator_origin)
<< test_case.description;
}
}
TEST_P(PrivateAggregationHostTest, FilteringIdMaxBytesValidated) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
const struct {
const char* description;
const size_t filtering_id_max_bytes;
bool expected_bind_result;
} kTestCases[] = {
{
"filtering_id_max_bytes zero",
0,
false,
},
{
"filtering_id_max_bytes minimum valid value",
1,
true,
},
{
"filtering_id_max_bytes maximum valid value",
AggregationServicePayloadContents::kMaximumFilteringIdMaxBytes,
true,
},
{
"filtering_id_max_bytes too large",
AggregationServicePayloadContents::kMaximumFilteringIdMaxBytes + 1,
false,
},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.description);
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
bool bind_result = host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/test_case.filtering_id_max_bytes ==
PrivateAggregationHost::kDefaultFilteringIdMaxBytes
? std::nullopt
: std::make_optional(base::Seconds(1)),
/*aggregation_coordinator_origin=*/std::nullopt,
/*filtering_id_max_bytes=*/test_case.filtering_id_max_bytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver());
EXPECT_EQ(bind_result, test_case.expected_bind_result);
}
}
TEST_P(PrivateAggregationHostTest, FilteringIdValidatedToFitInMaxBytes) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
const struct {
const char* description;
const size_t filtering_id_max_bytes;
const std::optional<uint64_t> filtering_id;
bool expected_to_be_valid;
std::optional<PrivateAggregationHost::FilteringIdStatus>
expected_filtering_id_histogram;
} kTestCases[] = {
{
"filtering_id null with default maxBytes",
1,
std::nullopt,
true,
PrivateAggregationHost::FilteringIdStatus::
kNoFilteringIdWithDefaultMaxBytes,
},
{
"filtering_id 0",
1,
0,
true,
PrivateAggregationHost::FilteringIdStatus::
kFilteringIdProvidedWithDefaultMaxBytes,
},
{
"filtering_id max for one byte",
1,
255,
true,
PrivateAggregationHost::FilteringIdStatus::
kFilteringIdProvidedWithDefaultMaxBytes,
},
{
"filtering_id too big",
1,
256,
false,
std::nullopt,
},
{
"filtering_id null with custom maxBytes",
3,
std::nullopt,
true,
PrivateAggregationHost::FilteringIdStatus::
kNoFilteringIdWithCustomMaxBytes,
},
{
"filtering_id max value for max maxBytes",
8,
std::numeric_limits<uint64_t>::max(),
true,
PrivateAggregationHost::FilteringIdStatus::
kFilteringIdProvidedWithCustomMaxBytes,
},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.description);
base::HistogramTester histogram;
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
bool bind_result = host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/test_case.filtering_id_max_bytes ==
PrivateAggregationHost::kDefaultFilteringIdMaxBytes
? std::nullopt
: std::make_optional(base::Seconds(1)),
/*aggregation_coordinator_origin=*/std::nullopt,
/*filtering_id_max_bytes=*/test_case.filtering_id_max_bytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver());
ASSERT_TRUE(bind_result);
std::optional<AggregatableReportRequest> validated_request;
if (test_case.expected_to_be_valid) {
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
} else {
EXPECT_CALL(mock_callback_, Run).Times(0);
}
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, test_case.filtering_id));
remote->ContributeToHistogram(std::move(contributions));
// The pipe should've been closed in case of a validation error.
remote.FlushForTesting();
EXPECT_EQ(remote.is_connected(), test_case.expected_to_be_valid);
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kPipeResultHistogram,
test_case.expected_to_be_valid
? PrivateAggregationHost::PipeResult::kReportSuccess
: PrivateAggregationHost::PipeResult::kFilteringIdInvalid,
1);
if (test_case.expected_filtering_id_histogram.has_value()) {
histogram.ExpectUniqueSample(
kFilteringIdStatusHistogram,
test_case.expected_filtering_id_histogram.value(), 1);
} else {
histogram.ExpectTotalCount(kFilteringIdStatusHistogram, 0);
}
EXPECT_EQ(validated_request.has_value(), test_case.expected_to_be_valid);
if (!validated_request.has_value()) {
continue;
}
ASSERT_EQ(validated_request->payload_contents().contributions.size(), 1u);
EXPECT_EQ(validated_request->payload_contents().filtering_id_max_bytes,
test_case.filtering_id_max_bytes);
EXPECT_EQ(
validated_request->payload_contents().contributions[0].filtering_id,
test_case.filtering_id);
}
}
TEST_P(PrivateAggregationHostTest,
DebugModeFeatureParamsAndSettingsCheckAppliedCorrectly) {
struct {
std::vector<base::test::FeatureRefAndParams> enabled_features;
bool expected_debug_mode_settings_check;
// No effect if `expected_debug_mode_settings_check` is false.
bool approve_debug_mode_settings_check = false;
bool call_enable_debug_mode = true;
AggregatableReportSharedInfo::DebugMode expected_debug_mode;
} kTestCases[] = {
{
.enabled_features = {{blink::features::kPrivateAggregationApi,
{{"debug_mode_enabled_at_all", "false"}}}},
.expected_debug_mode_settings_check = false,
.expected_debug_mode =
AggregatableReportSharedInfo::DebugMode::kDisabled,
},
{
.enabled_features =
{{kPrivateAggregationApiDebugModeRequires3pcEligibility, {}}},
.expected_debug_mode_settings_check = true,
.approve_debug_mode_settings_check = false,
.expected_debug_mode =
AggregatableReportSharedInfo::DebugMode::kDisabled,
},
{
.enabled_features =
{{kPrivateAggregationApiDebugModeRequires3pcEligibility, {}}},
.expected_debug_mode_settings_check = true,
.approve_debug_mode_settings_check = true,
.expected_debug_mode =
AggregatableReportSharedInfo::DebugMode::kEnabled,
},
{
.enabled_features =
{{kPrivateAggregationApiDebugModeRequires3pcEligibility, {}}},
.expected_debug_mode_settings_check = false,
.call_enable_debug_mode = false,
.expected_debug_mode =
AggregatableReportSharedInfo::DebugMode::kDisabled,
}};
for (auto& test_case : kTestCases) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeaturesAndParameters(
test_case.enabled_features, /*disabled_features=*/{});
base::HistogramTester histogram;
MockPrivateAggregationContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote.BindNewPipeAndPassReceiver()));
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_,
Run(_, _,
Property(&PrivateAggregationBudgetKey::caller_api,
PrivateAggregationCallerApi::kProtectedAudience),
NullReportBehavior::kDontSendReport))
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
if (test_case.call_enable_debug_mode) {
remote->EnableDebugMode(/*debug_key=*/nullptr);
}
EXPECT_CALL(browser_client, IsPrivateAggregationAllowed(_, kMainFrameOrigin,
kExampleOrigin, _))
.WillRepeatedly(testing::Return(true));
if (test_case.expected_debug_mode_settings_check) {
EXPECT_CALL(browser_client, IsPrivateAggregationDebugModeAllowed(
_, kMainFrameOrigin, kExampleOrigin))
.WillOnce(
testing::Return(test_case.approve_debug_mode_settings_check));
} else {
EXPECT_CALL(browser_client, IsPrivateAggregationDebugModeAllowed(
_, kMainFrameOrigin, kExampleOrigin))
.Times(0);
}
remote.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(validated_request);
EXPECT_EQ(validated_request->shared_info().debug_mode,
test_case.expected_debug_mode);
histogram.ExpectUniqueSample(
kPipeResultHistogram,
PrivateAggregationHost::PipeResult::kReportSuccess, 1);
}
}
TEST_P(PrivateAggregationHostTest, PipeClosedBeforeShutdown_NoHistogram) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
EXPECT_CALL(mock_callback_, Run).Times(0);
base::HistogramTester histogram;
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
EXPECT_TRUE(remote.is_connected());
remote.reset();
host_->FlushReceiverSetForTesting();
host_.reset();
histogram.ExpectTotalCount(
"PrivacySandbox.PrivateAggregation.Host.PipeOpenDurationOnShutdown", 0);
}
TEST_P(PrivateAggregationHostTest, PipeStillOpenAtShutdown_Histogram) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
EXPECT_CALL(mock_callback_, Run).Times(0);
base::HistogramTester histogram;
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
task_environment_.FastForwardBy(base::Minutes(10));
EXPECT_TRUE(remote.is_connected());
host_.reset();
histogram.ExpectUniqueTimeSample(
"PrivacySandbox.PrivateAggregation.Host.PipeOpenDurationOnShutdown",
base::Minutes(10), 1);
}
TEST_P(PrivateAggregationHostTest, TimeoutBeforeDisconnect) {
// Set the start time to be "on the minute".
base::Time on_the_minute_start_time =
base::Time() +
base::Time::Now().since_origin().CeilToMultiple(base::Minutes(1));
task_environment_.FastForwardBy(on_the_minute_start_time - base::Time::Now());
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
bool received_request = false;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior null_report_behavior) {
AggregatableReportRequest request = GenerateReportRequest(
std::move(generator), std::move(contributions));
received_request = true;
EXPECT_THAT(request.additional_fields(),
testing::ElementsAre(
testing::Pair("context_id", "example_context_id")));
EXPECT_TRUE(request.payload_contents().contributions.empty());
// Null reports have debug mode set only if the error reporting
// feature is enabled.
if (GetErrorReportingEnabledParam()) {
EXPECT_EQ(request.debug_key(), 1234);
EXPECT_EQ(request.shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
} else {
EXPECT_EQ(request.debug_key(), std::nullopt);
EXPECT_EQ(request.shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
}
EXPECT_EQ(request.shared_info().scheduled_report_time,
on_the_minute_start_time + base::Minutes(1) +
PrivateAggregationHost::kTimeForLocalProcessing);
EXPECT_EQ(budget_key.time_window().start_time(),
on_the_minute_start_time + base::Minutes(1));
}));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
remote->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
task_environment_.FastForwardBy(base::Seconds(59));
ASSERT_FALSE(received_request);
EXPECT_TRUE(remote.is_connected());
// At the timeout time, the reports are sent and the pipe is closed.
task_environment_.FastForwardBy(base::Seconds(1));
ASSERT_TRUE(received_request);
EXPECT_FALSE(remote.is_connected());
remote.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kTimeoutResultHistogram,
PrivateAggregationHost::TimeoutResult::kOccurredBeforeRemoteDisconnection,
1);
}
TEST_P(PrivateAggregationHostTest, TimeoutAfterDisconnect) {
// Set the start time to be "on the minute".
base::Time on_the_minute_start_time =
base::Time() +
base::Time::Now().since_origin().CeilToMultiple(base::Minutes(1));
task_environment_.FastForwardBy(on_the_minute_start_time - base::Time::Now());
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
bool received_request = false;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior null_report_behavior) {
AggregatableReportRequest request = GenerateReportRequest(
std::move(generator), std::move(contributions));
received_request = true;
EXPECT_THAT(request.additional_fields(),
testing::ElementsAre(
testing::Pair("context_id", "example_context_id")));
EXPECT_TRUE(request.payload_contents().contributions.empty());
// Null reports have debug mode set only if the error reporting
// feature is enabled.
if (GetErrorReportingEnabledParam()) {
EXPECT_EQ(request.debug_key(), 1234);
EXPECT_EQ(request.shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
} else {
EXPECT_EQ(request.debug_key(), std::nullopt);
EXPECT_EQ(request.shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
}
// `request` should have report scheduled 1s from now.
CHECK_EQ(base::Time::Now() + base::Seconds(1),
on_the_minute_start_time + base::Minutes(1));
EXPECT_EQ(request.shared_info().scheduled_report_time,
on_the_minute_start_time + base::Minutes(1) +
PrivateAggregationHost::kTimeForLocalProcessing);
// The start time for budgeting should be based off the current
// time, instead of the desired timeout time.
EXPECT_EQ(budget_key.time_window().start_time(),
on_the_minute_start_time);
}));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
remote->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
task_environment_.FastForwardBy(base::Seconds(59));
ASSERT_FALSE(received_request);
EXPECT_TRUE(remote.is_connected());
remote.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(received_request);
histogram.ExpectUniqueSample(
kTimeoutResultHistogram,
PrivateAggregationHost::TimeoutResult::kOccurredAfterRemoteDisconnection,
1);
}
// Test the scenario that the disconnect happens before the timer fires, but we
// find the remaining time is negative. This can happen if enough time passes
// between the disconnect and the point when we compute the remaining time that
// the current time exceeds the original timer deadline. Viewed as a timeline:
//
// T1 T2 T3
// ----|---------------------|---------------------|--------------->
// Disconnect Timer deadline Compute remaining time
//
TEST_P(PrivateAggregationHostTest,
TimeoutAfterDisconnectTimeRemainingNegative) {
// Set the start time to be "on the minute".
base::Time on_the_minute_start_time =
base::Time() +
base::Time::Now().since_origin().CeilToMultiple(base::Minutes(1));
task_environment_.FastForwardBy(on_the_minute_start_time - base::Time::Now());
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
bool received_request = false;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
PrivateAggregationPendingContributions::Wrapper contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationHost::NullReportBehavior null_report_behavior) {
AggregatableReportRequest request = GenerateReportRequest(
std::move(generator), std::move(contributions));
received_request = true;
EXPECT_THAT(request.additional_fields(),
testing::ElementsAre(
testing::Pair("context_id", "example_context_id")));
EXPECT_TRUE(request.payload_contents().contributions.empty());
EXPECT_EQ(request.debug_key(), std::nullopt);
EXPECT_EQ(request.shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
// `request` should have report scheduled now (with some additional
// buffer for local processing).
CHECK_EQ(base::Time::Now(),
on_the_minute_start_time + base::Seconds(61));
EXPECT_EQ(request.shared_info().scheduled_report_time,
on_the_minute_start_time + base::Seconds(61) +
PrivateAggregationHost::kTimeForLocalProcessing);
// The start time for budgeting should be based off the current
// time, instead of the desired timeout time.
EXPECT_EQ(budget_key.time_window().start_time(),
base::Time::Now() - base::Seconds(1));
}));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
CHECK_EQ(base::Time::Now(), on_the_minute_start_time);
// Run pending tasks by fast-forwarding. The timer will not fire because its
// desired run time will still be in the future.
task_environment_.FastForwardBy(base::Seconds(59));
// Without giving the timer a chance to fire, advance the clock past its
// desired run time.
task_environment_.AdvanceClock(base::Seconds(2));
ASSERT_FALSE(received_request);
EXPECT_TRUE(remote.is_connected());
remote.reset();
task_environment_.RunUntilIdle();
ASSERT_TRUE(received_request);
CHECK_EQ(base::Time::Now(), on_the_minute_start_time + base::Seconds(61));
histogram.ExpectUniqueSample(
kTimeoutResultHistogram,
PrivateAggregationHost::TimeoutResult::kOccurredAfterRemoteDisconnection,
1);
}
TEST_P(PrivateAggregationHostTest, TimeoutBeforeDisconnectForTwoHosts) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote1;
mojo::Remote<blink::mojom::PrivateAggregationHost> remote2;
std::optional<AggregatableReportRequest> validated_request1;
std::optional<AggregatableReportRequest> validated_request2;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_request1))
.WillOnce(GenerateAndSaveReportRequest(&validated_request2));
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote1.BindNewPipeAndPassReceiver()));
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Seconds(61),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote2.BindNewPipeAndPassReceiver()));
remote1->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
task_environment_.FastForwardBy(base::Seconds(59));
ASSERT_FALSE(validated_request1.has_value());
ASSERT_FALSE(validated_request2.has_value());
remote2->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
// Timeout reached for remote1.
task_environment_.FastForwardBy(base::Seconds(1));
ASSERT_TRUE(validated_request1.has_value());
ASSERT_FALSE(validated_request2.has_value());
EXPECT_FALSE(remote1.is_connected());
EXPECT_TRUE(remote2.is_connected());
EXPECT_THAT(
validated_request1->additional_fields(),
testing::ElementsAre(testing::Pair("context_id", "example_context_id")));
EXPECT_TRUE(validated_request1->payload_contents().contributions.empty());
// Null reports have debug mode set only if the error reporting feature is
// enabled.
if (GetErrorReportingEnabledParam()) {
EXPECT_EQ(validated_request1->debug_key(), 1234);
EXPECT_EQ(validated_request1->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
} else {
EXPECT_EQ(validated_request1->debug_key(), std::nullopt);
EXPECT_EQ(validated_request1->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
}
EXPECT_EQ(
validated_request1->shared_info().scheduled_report_time,
base::Time::Now() + PrivateAggregationHost::kTimeForLocalProcessing);
// Timeout reached for remote2.
task_environment_.FastForwardBy(base::Seconds(1));
ASSERT_TRUE(validated_request2.has_value());
EXPECT_FALSE(remote2.is_connected());
EXPECT_THAT(
validated_request2->additional_fields(),
testing::ElementsAre(testing::Pair("context_id", "example_context_id")));
EXPECT_TRUE(validated_request2->payload_contents().contributions.empty());
// Null reports have debug mode set only if the error reporting feature is
// enabled.
if (GetErrorReportingEnabledParam()) {
EXPECT_EQ(validated_request2->debug_key(), 1234);
EXPECT_EQ(validated_request2->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
} else {
EXPECT_EQ(validated_request2->debug_key(), std::nullopt);
EXPECT_EQ(validated_request2->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
}
EXPECT_EQ(
validated_request2->shared_info().scheduled_report_time,
base::Time::Now() + PrivateAggregationHost::kTimeForLocalProcessing);
remote1.reset();
remote2.reset();
host_->FlushReceiverSetForTesting();
histogram.ExpectUniqueSample(
kTimeoutResultHistogram,
PrivateAggregationHost::TimeoutResult::kOccurredBeforeRemoteDisconnection,
2);
}
TEST_P(PrivateAggregationHostTest, TimeoutAfterDisconnectForTwoHosts) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote1;
mojo::Remote<blink::mojom::PrivateAggregationHost> remote2;
std::optional<AggregatableReportRequest> validated_request1;
std::optional<AggregatableReportRequest> validated_request2;
EXPECT_CALL(mock_callback_, Run)
.WillOnce(GenerateAndSaveReportRequest(&validated_request1))
.WillOnce(GenerateAndSaveReportRequest(&validated_request2));
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote1.BindNewPipeAndPassReceiver()));
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Seconds(61),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote2.BindNewPipeAndPassReceiver()));
remote1->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
remote2->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
task_environment_.FastForwardBy(base::Seconds(59));
ASSERT_FALSE(validated_request1.has_value());
ASSERT_FALSE(validated_request2.has_value());
remote1.reset();
remote2.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(validated_request1.has_value());
ASSERT_TRUE(validated_request2.has_value());
// `validated_request1` should have report scheduled 1s from now.
EXPECT_THAT(
validated_request1->additional_fields(),
testing::ElementsAre(testing::Pair("context_id", "example_context_id")));
EXPECT_TRUE(validated_request1->payload_contents().contributions.empty());
// Null reports have debug mode set only if the error reporting feature is
// enabled.
if (GetErrorReportingEnabledParam()) {
EXPECT_EQ(validated_request1->debug_key(), 1234);
EXPECT_EQ(validated_request1->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
} else {
EXPECT_EQ(validated_request1->debug_key(), std::nullopt);
EXPECT_EQ(validated_request1->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
}
EXPECT_EQ(validated_request1->shared_info().scheduled_report_time,
base::Time::Now() + base::Seconds(1) +
PrivateAggregationHost::kTimeForLocalProcessing);
// `validated_request2` should have report scheduled 2s from now.
EXPECT_THAT(
validated_request2->additional_fields(),
testing::ElementsAre(testing::Pair("context_id", "example_context_id")));
EXPECT_TRUE(validated_request2->payload_contents().contributions.empty());
// Null reports have debug mode set only if the error reporting feature is
// enabled.
if (GetErrorReportingEnabledParam()) {
EXPECT_EQ(validated_request2->debug_key(), 1234);
EXPECT_EQ(validated_request2->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kEnabled);
} else {
EXPECT_EQ(validated_request2->debug_key(), std::nullopt);
EXPECT_EQ(validated_request2->shared_info().debug_mode,
AggregatableReportSharedInfo::DebugMode::kDisabled);
}
EXPECT_EQ(validated_request2->shared_info().scheduled_report_time,
base::Time::Now() + base::Seconds(2) +
PrivateAggregationHost::kTimeForLocalProcessing);
histogram.ExpectUniqueSample(
kTimeoutResultHistogram,
PrivateAggregationHost::TimeoutResult::kOccurredAfterRemoteDisconnection,
2);
}
TEST_P(PrivateAggregationHostTest, TimeoutCanceledDueToError) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
remote->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
// This second call to EnableDebugMode() will fail the mojom validation.
remote->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/5678u));
remote.FlushForTesting();
EXPECT_FALSE(remote.is_connected());
host_.reset();
histogram.ExpectUniqueSample(
kTimeoutResultHistogram,
PrivateAggregationHost::TimeoutResult::kCanceledDueToError, 1);
}
TEST_P(PrivateAggregationHostTest,
TimeoutStillScheduledOnShutdownWithPipeOpen) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
remote->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
host_->FlushReceiverSetForTesting();
EXPECT_TRUE(remote.is_connected());
host_.reset();
histogram.ExpectUniqueSample(
kTimeoutResultHistogram,
PrivateAggregationHost::TimeoutResult::kStillScheduledOnShutdown, 1);
}
TEST_P(PrivateAggregationHostTest,
TimeoutStillScheduledOnShutdownWithPipeOpenForTwoHosts) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote1;
mojo::Remote<blink::mojom::PrivateAggregationHost> remote2;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote1.BindNewPipeAndPassReceiver()));
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kSharedStorage, "example_context_id",
/*timeout=*/base::Minutes(1),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt,
remote2.BindNewPipeAndPassReceiver()));
remote1->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
remote2->EnableDebugMode(blink::mojom::DebugKey::New(/*value=*/1234u));
task_environment_.FastForwardBy(base::Seconds(59));
EXPECT_TRUE(remote1.is_connected());
EXPECT_TRUE(remote2.is_connected());
host_.reset();
histogram.ExpectUniqueSample(
kTimeoutResultHistogram,
PrivateAggregationHost::TimeoutResult::kStillScheduledOnShutdown, 2);
}
// `GetEffectiveMaxContributions()` will crash when the feature controlling the
// web-visible `maxContributions` field is disabled, yet somehow the
// `requested_max_contributions` parameter contains a value.
TEST_P(PrivateAggregationHostTest,
GetEffectiveMaxContributionsFeatureDisabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
blink::features::kPrivateAggregationApiMaxContributions);
EXPECT_EQ(PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kSharedStorage, std::nullopt),
20u);
EXPECT_EQ(PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kProtectedAudience, std::nullopt),
100u);
EXPECT_DEATH_IF_SUPPORTED(
PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kSharedStorage, 100u),
"");
EXPECT_DEATH_IF_SUPPORTED(
PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kProtectedAudience, 100u),
"");
}
TEST_P(PrivateAggregationHostTest, GetEffectiveMaxContributionsCrashOnZero) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
blink::features::kPrivateAggregationApiMaxContributions);
EXPECT_DEATH_IF_SUPPORTED(
PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kProtectedAudience, 0u),
"");
EXPECT_DEATH_IF_SUPPORTED(
PrivateAggregationHost::GetEffectiveMaxContributions(
PrivateAggregationCallerApi::kSharedStorage, 0u),
"");
}
// `GetEffectiveMaxContributions()` should correctly handle the
// `requested_max_contributions` parameter when the feature controlling the
// web-visible `maxContributions` field is enabled.
TEST_P(PrivateAggregationHostTest, GetEffectiveMaxContributions) {
using enum PrivateAggregationCallerApi;
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
blink::features::kPrivateAggregationApiMaxContributions);
struct TestCase {
PrivateAggregationCallerApi caller_api;
std::optional<size_t> requested_max_contributions;
size_t expected;
};
static constexpr TestCase kTestCases[] = {
// Callers that do not specify `requested_max_contributions` get the
// API-specific default.
{kSharedStorage, std::nullopt, 20u},
{kProtectedAudience, std::nullopt, 100u},
// Requests for 1-1000 contributions get exactly that many contributions.
{kSharedStorage, 1u, 1u},
{kProtectedAudience, 1u, 1u},
{kSharedStorage, 20u, 20u},
{kProtectedAudience, 20u, 20u},
{kSharedStorage, 1000u, 1000u},
{kProtectedAudience, 1000u, 1000u},
// Requests for more than 1000 contributions get clamped to 1000.
{kSharedStorage, 1001u, 1000u},
{kProtectedAudience, 1001, 1000u},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(testing::Message()
<< "caller_api: "
<< testing::PrintToString(test_case.caller_api)
<< ", requested_max_contributions: "
<< testing::PrintToString(
test_case.requested_max_contributions)
<< ", expected: " << test_case.expected);
EXPECT_EQ(PrivateAggregationHost::GetEffectiveMaxContributions(
test_case.caller_api, test_case.requested_max_contributions),
test_case.expected);
}
}
TEST_F(PrivateAggregationHostErrorReportingEnabledTest,
SingleContributeToHistogramOnEvent_PendingContributionsIsCorrect) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
std::optional<PrivateAggregationHost::ReportRequestGenerator> saved_generator;
std::optional<PrivateAggregationPendingContributions> saved_contributions;
EXPECT_CALL(mock_callback_,
Run(_, _,
Property(&PrivateAggregationBudgetKey::caller_api,
PrivateAggregationCallerApi::kProtectedAudience),
NullReportBehavior::kDontSendReport))
.WillOnce(SaveGeneratorAndPendingContributions(&saved_generator,
&saved_contributions));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogramOnEvent(
blink::mojom::PrivateAggregationErrorEvent::kPendingReportLimitReached,
std::move(contributions));
// Should not get a PrivateAggregationPendingContributions until after the
// remote is disconnected.
remote.FlushForTesting();
EXPECT_TRUE(remote.is_connected());
EXPECT_FALSE(saved_contributions);
remote.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(saved_contributions);
EXPECT_TRUE(saved_contributions->are_contributions_finalized());
EXPECT_TRUE(saved_contributions->unconditional_contributions().empty());
EXPECT_THAT(saved_contributions->GetConditionalContributionsForTesting(),
testing::UnorderedElementsAre(testing::Pair(
blink::mojom::PrivateAggregationErrorEvent::
kPendingReportLimitReached,
testing::UnorderedElementsAre(
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/123, /*value=*/456,
/*filtering_id=*/std::nullopt)))));
saved_contributions->CompileFinalUnmergedContributions(
/*test_budgeter_results=*/{},
PrivateAggregationPendingContributions::PendingReportLimitResult::
kAtLimit,
PrivateAggregationPendingContributions::NullReportBehavior::
kSendNullReport);
AggregatableReportRequest report_request =
std::move(*saved_generator)
.Run(std::move(*saved_contributions)
.TakeFinalContributions(
{PrivateAggregationPendingContributions::BudgeterResult::
kApproved}));
// We only do some basic validation for the scheduled report time and report
// ID as they are not deterministic and will be copied to `expected_request`.
// We're using `MOCK_TIME` so we can be sure no time has advanced.
base::Time now = base::Time::Now();
EXPECT_GE(report_request.shared_info().scheduled_report_time,
now + base::Minutes(10) +
PrivateAggregationHost::kTimeForLocalProcessing);
EXPECT_LE(
report_request.shared_info().scheduled_report_time,
now + base::Hours(1) + PrivateAggregationHost::kTimeForLocalProcessing);
EXPECT_TRUE(report_request.shared_info().report_id.is_valid());
// We only made one contribution, and padding would be added later on by
// `AggregatableReport::Provider::CreateFromRequestAndPublicKeys()`.
EXPECT_EQ(report_request.payload_contents().contributions.size(), 1u);
std::optional<AggregatableReportRequest> expected_request =
AggregatableReportRequest::Create(
AggregationServicePayloadContents(
AggregationServicePayloadContents::Operation::kHistogram,
{blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/123, /*value=*/456,
/*filtering_id=*/std::nullopt)},
/*aggregation_coordinator_origin=*/std::nullopt,
/*max_contributions_allowed=*/20u,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes),
AggregatableReportSharedInfo(
report_request.shared_info().scheduled_report_time,
report_request.shared_info().report_id,
/*reporting_origin=*/kExampleOrigin,
AggregatableReportSharedInfo::DebugMode::kDisabled,
/*additional_fields=*/base::Value::Dict(),
/*api_version=*/"1.0",
/*api_identifier=*/"protected-audience"),
AggregatableReportRequest::DelayType::ScheduledWithFullDelay,
/*reporting_path=*/
"/.well-known/private-aggregation/report-protected-audience");
ASSERT_TRUE(expected_request);
EXPECT_TRUE(aggregation_service::ReportRequestsEqual(
report_request, expected_request.value()));
histogram.ExpectUniqueSample(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
1);
ExpectNumberOfContributionMergeKeysHistogram(
histogram, 1, PrivateAggregationCallerApi::kProtectedAudience,
/*is_reduced_delay=*/false);
ExpectNumberOfFinalUnmergedContributionsHistogram(
histogram, 1, PrivateAggregationCallerApi::kProtectedAudience,
/*is_reduced_delay=*/false);
ExpectTruncationResultHistogram(
histogram,
PrivateAggregationPendingContributions::TruncationResult::kNoTruncation);
}
TEST_F(
PrivateAggregationHostErrorReportingEnabledTest,
MultipleContributeToHistogramOnEventCalls_PendingContributionsIsCorrect) {
base::HistogramTester histogram;
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
std::optional<PrivateAggregationHost::ReportRequestGenerator> saved_generator;
std::optional<PrivateAggregationPendingContributions> saved_contributions;
EXPECT_CALL(mock_callback_,
Run(_, _,
Property(&PrivateAggregationBudgetKey::caller_api,
PrivateAggregationCallerApi::kProtectedAudience),
NullReportBehavior::kDontSendReport))
.WillOnce(SaveGeneratorAndPendingContributions(&saved_generator,
&saved_contributions));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
pending_report_limit_contributions;
pending_report_limit_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/1, /*value=*/2, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogramOnEvent(
blink::mojom::PrivateAggregationErrorEvent::kPendingReportLimitReached,
std::move(pending_report_limit_contributions));
// Note that kEmptyReportDropped should not be triggered as there is an
// unconditional contribution later.
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
empty_report_contributions;
empty_report_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/3, /*value=*/4, /*filtering_id=*/5));
remote->ContributeToHistogramOnEvent(
blink::mojom::PrivateAggregationErrorEvent::kEmptyReportDropped,
std::move(empty_report_contributions));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
unconditional_contributions;
unconditional_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/6, /*value=*/7, /*filtering_id=*/8));
remote->ContributeToHistogram(std::move(unconditional_contributions));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
already_triggered_contributions;
already_triggered_contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/9, /*value=*/10, /*filtering_id=*/11));
remote->ContributeToHistogramOnEvent(
blink::mojom::PrivateAggregationErrorEvent::
kAlreadyTriggeredExternalError,
std::move(already_triggered_contributions));
// Should not get a PrivateAggregationPendingContributions until after the
// remote is disconnected.
remote.FlushForTesting();
EXPECT_TRUE(remote.is_connected());
EXPECT_FALSE(saved_contributions);
remote.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(saved_contributions);
EXPECT_TRUE(saved_contributions->are_contributions_finalized());
EXPECT_THAT(saved_contributions->unconditional_contributions(),
testing::UnorderedElementsAre(
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/6, /*value=*/7, /*filtering_id=*/8)));
EXPECT_THAT(
saved_contributions->GetConditionalContributionsForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::mojom::PrivateAggregationErrorEvent::
kPendingReportLimitReached,
testing::UnorderedElementsAre(
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/1, /*value=*/2,
/*filtering_id=*/std::nullopt))),
testing::Pair(
blink::mojom::PrivateAggregationErrorEvent::kEmptyReportDropped,
testing::UnorderedElementsAre(
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/3, /*value=*/4, /*filtering_id=*/5))),
testing::Pair(
blink::mojom::PrivateAggregationErrorEvent::
kAlreadyTriggeredExternalError,
testing::UnorderedElementsAre(
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/9, /*value=*/10, /*filtering_id=*/11)))));
saved_contributions->CompileFinalUnmergedContributions(
/*test_budgeter_results=*/{PrivateAggregationPendingContributions::
BudgeterResult::kApproved},
PrivateAggregationPendingContributions::PendingReportLimitResult::
kAtLimit,
PrivateAggregationPendingContributions::NullReportBehavior::
kSendNullReport);
AggregatableReportRequest report_request =
std::move(*saved_generator)
.Run(std::move(*saved_contributions)
.TakeFinalContributions(
{PrivateAggregationPendingContributions::BudgeterResult::
kApproved,
PrivateAggregationPendingContributions::BudgeterResult::
kApproved,
PrivateAggregationPendingContributions::BudgeterResult::
kApproved}));
// We only do some basic validation for the scheduled report time and report
// ID as they are not deterministic and will be copied to `expected_request`.
// We're using `MOCK_TIME` so we can be sure no time has advanced.
base::Time now = base::Time::Now();
EXPECT_GE(report_request.shared_info().scheduled_report_time,
now + base::Minutes(10) +
PrivateAggregationHost::kTimeForLocalProcessing);
EXPECT_LE(
report_request.shared_info().scheduled_report_time,
now + base::Hours(1) + PrivateAggregationHost::kTimeForLocalProcessing);
EXPECT_TRUE(report_request.shared_info().report_id.is_valid());
// Padding should be added later on by
// `AggregatableReport::Provider::CreateFromRequestAndPublicKeys()`.
EXPECT_EQ(report_request.payload_contents().contributions.size(), 3u);
std::optional<AggregatableReportRequest> expected_request =
AggregatableReportRequest::Create(
AggregationServicePayloadContents(
AggregationServicePayloadContents::Operation::kHistogram,
{
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/1, /*value=*/2,
/*filtering_id=*/std::nullopt),
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/9, /*value=*/10,
/*filtering_id=*/11),
blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/6, /*value=*/7,
/*filtering_id=*/8),
},
/*aggregation_coordinator_origin=*/std::nullopt,
/*max_contributions_allowed=*/20u,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes),
AggregatableReportSharedInfo(
report_request.shared_info().scheduled_report_time,
report_request.shared_info().report_id,
/*reporting_origin=*/kExampleOrigin,
AggregatableReportSharedInfo::DebugMode::kDisabled,
/*additional_fields=*/base::Value::Dict(),
/*api_version=*/"1.0",
/*api_identifier=*/"protected-audience"),
AggregatableReportRequest::DelayType::ScheduledWithFullDelay,
/*reporting_path=*/
"/.well-known/private-aggregation/report-protected-audience");
ASSERT_TRUE(expected_request);
EXPECT_TRUE(aggregation_service::ReportRequestsEqual(
report_request, expected_request.value()));
histogram.ExpectUniqueSample(
kPipeResultHistogram, PrivateAggregationHost::PipeResult::kReportSuccess,
1);
ExpectNumberOfContributionMergeKeysHistogram(
histogram, 3, PrivateAggregationCallerApi::kProtectedAudience,
/*is_reduced_delay=*/false);
ExpectNumberOfFinalUnmergedContributionsHistogram(
histogram, 3, PrivateAggregationCallerApi::kProtectedAudience,
/*is_reduced_delay=*/false);
ExpectTruncationResultHistogram(
histogram,
PrivateAggregationPendingContributions::TruncationResult::kNoTruncation);
}
class PrivateAggregationHostDeveloperModeTest
: public PrivateAggregationHostTest {
public:
PrivateAggregationHostDeveloperModeTest() {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kPrivateAggregationDeveloperMode);
}
};
INSTANTIATE_TEST_SUITE_P(,
PrivateAggregationHostDeveloperModeTest,
testing::Bool(),
[](auto& info) {
return info.param ? "ErrorReportingEnabled"
: "ErrorReportingDisabled";
});
TEST_P(PrivateAggregationHostDeveloperModeTest,
ContributeToHistogram_ScheduledReportTimeIsNotDelayed) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/std::nullopt,
/*timeout=*/std::nullopt,
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_,
Run(_, _,
Property(&PrivateAggregationBudgetKey::caller_api,
PrivateAggregationCallerApi::kProtectedAudience),
NullReportBehavior::kDontSendReport))
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(validated_request);
// We're using `MOCK_TIME` so we can be sure no time has advanced.
EXPECT_EQ(
validated_request->shared_info().scheduled_report_time,
base::Time::Now() + PrivateAggregationHost::kTimeForLocalProcessing);
}
TEST_P(PrivateAggregationHostDeveloperModeTest,
TimeoutSet_ScheduledReportTimeIsNotDelayed) {
const url::Origin kExampleOrigin =
url::Origin::Create(GURL("https://example.com"));
const url::Origin kMainFrameOrigin =
url::Origin::Create(GURL("https://main_frame.com"));
mojo::Remote<blink::mojom::PrivateAggregationHost> remote;
EXPECT_TRUE(host_->BindNewReceiver(
kExampleOrigin, kMainFrameOrigin,
PrivateAggregationCallerApi::kProtectedAudience,
/*context_id=*/"example_context_id",
/*timeout=*/base::Seconds(30),
/*aggregation_coordinator_origin=*/std::nullopt,
PrivateAggregationHost::kDefaultFilteringIdMaxBytes,
/*max_contributions=*/std::nullopt, remote.BindNewPipeAndPassReceiver()));
std::optional<AggregatableReportRequest> validated_request;
EXPECT_CALL(mock_callback_,
Run(_, _,
Property(&PrivateAggregationBudgetKey::caller_api,
PrivateAggregationCallerApi::kProtectedAudience),
NullReportBehavior::kSendNullReport))
.WillOnce(GenerateAndSaveReportRequest(&validated_request));
std::vector<blink::mojom::AggregatableReportHistogramContributionPtr>
contributions;
contributions.push_back(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123, /*value=*/456, /*filtering_id=*/std::nullopt));
remote->ContributeToHistogram(std::move(contributions));
remote.reset();
host_->FlushReceiverSetForTesting();
ASSERT_TRUE(validated_request);
// We're using `MOCK_TIME` so we can be sure no time has advanced. The
// requested timeout is ignored because developer mode is enabled.
EXPECT_EQ(
validated_request->shared_info().scheduled_report_time,
base::Time::Now() + PrivateAggregationHost::kTimeForLocalProcessing);
}
} // namespace
} // namespace content