| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/aggregation_service/aggregatable_report.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <array> |
| #include <limits> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/containers/flat_map.h" |
| #include "base/containers/span.h" |
| #include "base/json/json_writer.h" |
| #include "base/numerics/byte_conversions.h" |
| #include "base/numerics/ostream_operators.h" |
| #include "base/strings/abseil_string_number_conversions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/time/time.h" |
| #include "base/uuid.h" |
| #include "base/values.h" |
| #include "components/aggregation_service/aggregation_coordinator_utils.h" |
| #include "components/cbor/reader.h" |
| #include "components/cbor/values.h" |
| #include "content/browser/aggregation_service/aggregatable_report.h" |
| #include "content/browser/aggregation_service/aggregation_service_test_utils.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/numeric/int128.h" |
| #include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| testing::AssertionResult CborMapContainsKeyAndType( |
| const cbor::Value::MapValue& map, |
| std::string_view key, |
| cbor::Value::Type value_type) { |
| const auto it = map.find(cbor::Value(key)); |
| if (it == map.end()) { |
| return testing::AssertionFailure() |
| << "Expected key cbor::Value(\"" << key << "\") to be in map"; |
| } |
| |
| if (it->second.type() != value_type) { |
| return testing::AssertionFailure() |
| << "Expected value to have type " << static_cast<int>(value_type) |
| << ", actual: " << static_cast<int>(it->second.type()); |
| } |
| |
| return testing::AssertionSuccess(); |
| } |
| |
| std::vector<blink::mojom::AggregatableReportHistogramContribution> |
| PadContributions( |
| std::vector<blink::mojom::AggregatableReportHistogramContribution> |
| contributions, |
| size_t max_contributions_allowed) { |
| EXPECT_LE(contributions.size(), max_contributions_allowed); |
| for (size_t i = contributions.size(); i < max_contributions_allowed; ++i) { |
| contributions.emplace_back(/*bucket=*/0, /*value=*/0, |
| /*filtering_id=*/std::nullopt); |
| } |
| return contributions; |
| } |
| |
| // Tests that the report has the expected format, matches the provided details, |
| // and is decryptable by the provided keys. Note that |
| // `expected_payload_contents` is not expected to have its contributions already |
| // padded. |
| void VerifyReport( |
| const std::optional<AggregatableReport>& report, |
| const AggregationServicePayloadContents& expected_payload_contents, |
| const AggregatableReportSharedInfo& expected_shared_info, |
| const std::optional<uint64_t>& expected_debug_key, |
| const base::flat_map<std::string, std::string>& expected_additional_fields, |
| const aggregation_service::TestHpkeKey& encryption_key) { |
| ASSERT_TRUE(report.has_value()); |
| ASSERT_TRUE(report->payload().has_value()); |
| |
| std::string expected_serialized_shared_info = |
| expected_shared_info.SerializeAsJson(); |
| EXPECT_EQ(report->shared_info(), expected_serialized_shared_info); |
| |
| EXPECT_EQ(report->debug_key(), expected_debug_key); |
| EXPECT_EQ(report->additional_fields(), expected_additional_fields); |
| |
| const AggregatableReport::AggregationServicePayload& payload = |
| *report->payload(); |
| |
| std::vector<blink::mojom::AggregatableReportHistogramContribution> |
| expected_contributions = |
| PadContributions(expected_payload_contents.contributions, |
| expected_payload_contents.max_contributions_allowed); |
| |
| EXPECT_EQ(payload.key_id, encryption_key.key_id()); |
| |
| std::vector<uint8_t> decrypted_payload = |
| aggregation_service::DecryptPayloadWithHpke( |
| payload.payload, encryption_key.full_hpke_key(), |
| expected_serialized_shared_info); |
| ASSERT_FALSE(decrypted_payload.empty()); |
| |
| if (expected_shared_info.debug_mode == |
| AggregatableReportSharedInfo::DebugMode::kEnabled) { |
| ASSERT_TRUE(payload.debug_cleartext_payload.has_value()); |
| EXPECT_EQ(payload.debug_cleartext_payload.value(), decrypted_payload); |
| } else { |
| EXPECT_FALSE(payload.debug_cleartext_payload.has_value()); |
| } |
| |
| std::optional<cbor::Value> deserialized_payload = |
| cbor::Reader::Read(decrypted_payload); |
| ASSERT_TRUE(deserialized_payload.has_value()); |
| ASSERT_TRUE(deserialized_payload->is_map()); |
| const cbor::Value::MapValue& payload_map = deserialized_payload->GetMap(); |
| |
| EXPECT_EQ(payload_map.size(), 2UL); |
| |
| ASSERT_TRUE(CborMapContainsKeyAndType(payload_map, "operation", |
| cbor::Value::Type::STRING)); |
| EXPECT_EQ(payload_map.at(cbor::Value("operation")).GetString(), "histogram"); |
| |
| ASSERT_TRUE( |
| CborMapContainsKeyAndType(payload_map, "data", cbor::Value::Type::ARRAY)); |
| const cbor::Value::ArrayValue& data_array = |
| payload_map.at(cbor::Value("data")).GetArray(); |
| |
| ASSERT_EQ(data_array.size(), expected_contributions.size()); |
| for (size_t j = 0; j < data_array.size(); ++j) { |
| ASSERT_TRUE(data_array[j].is_map()); |
| const cbor::Value::MapValue& data_map = data_array[j].GetMap(); |
| |
| ASSERT_TRUE(CborMapContainsKeyAndType(data_map, "bucket", |
| cbor::Value::Type::BYTE_STRING)); |
| const cbor::Value::BinaryValue& bucket_byte_string = |
| data_map.at(cbor::Value("bucket")).GetBytestring(); |
| EXPECT_EQ(bucket_byte_string.size(), 16u); // 16 bytes = 128 bits |
| |
| // TODO(crbug.com/40215445): Replace with |
| // `base::U128FromBigEndian()` when available. |
| absl::uint128 bucket; |
| base::HexStringToUInt128(base::HexEncode(bucket_byte_string), &bucket); |
| EXPECT_EQ(bucket, expected_contributions[j].bucket); |
| |
| ASSERT_TRUE(CborMapContainsKeyAndType(data_map, "value", |
| cbor::Value::Type::BYTE_STRING)); |
| const cbor::Value::BinaryValue& value_byte_string = |
| data_map.at(cbor::Value("value")).GetBytestring(); |
| EXPECT_EQ(value_byte_string.size(), 4u); // 4 bytes = 32 bits |
| |
| uint32_t value = base::U32FromBigEndian( |
| base::as_byte_span(value_byte_string).first<4u>()); |
| EXPECT_EQ(int64_t{value}, expected_contributions[j].value); |
| |
| ASSERT_TRUE(CborMapContainsKeyAndType(data_map, "id", |
| cbor::Value::Type::BYTE_STRING)); |
| const cbor::Value::BinaryValue& filtering_id_byte_string = |
| data_map.at(cbor::Value("id")).GetBytestring(); |
| ASSERT_EQ(filtering_id_byte_string.size(), |
| expected_payload_contents.filtering_id_max_bytes); |
| |
| std::array<uint8_t, 8u> padded_filtering_id_bytestring; |
| padded_filtering_id_bytestring.fill(0); |
| base::as_writable_byte_span(padded_filtering_id_bytestring) |
| .last(expected_payload_contents.filtering_id_max_bytes) |
| .copy_from(filtering_id_byte_string); |
| |
| CHECK_LE(expected_payload_contents.filtering_id_max_bytes, 8u); |
| uint64_t filtering_id = |
| base::U64FromBigEndian(base::span(padded_filtering_id_bytestring)); |
| |
| EXPECT_EQ(filtering_id, expected_contributions[j].filtering_id.value_or(0)); |
| } |
| } |
| |
| class AggregatableReportTest : public ::testing::Test { |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| ::aggregation_service::ScopedAggregationCoordinatorAllowlistForTesting |
| scoped_coordinator_allowlist_{ |
| {url::Origin::Create(GURL("https://a.test")), |
| url::Origin::Create(GURL("https://b.test"))}}; |
| }; |
| |
| TEST_F(AggregatableReportTest, ValidRequest_ValidReportReturned) { |
| AggregatableReportRequest request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents expected_payload_contents = |
| request.payload_contents(); |
| AggregatableReportSharedInfo expected_shared_info = |
| request.shared_info().Clone(); |
| |
| aggregation_service::TestHpkeKey hpke_key("id123"); |
| |
| std::optional<AggregatableReport> report = |
| AggregatableReport::Provider().CreateFromRequestAndPublicKey( |
| std::move(request), hpke_key.GetPublicKey()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| VerifyReport(report, expected_payload_contents, expected_shared_info, |
| /*expected_debug_key=*/std::nullopt, |
| /*expected_additional_fields=*/{}, hpke_key)); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| ValidMultipleContributionsRequest_ValidReportReturned) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents expected_payload_contents = |
| example_request.payload_contents(); |
| expected_payload_contents.contributions = { |
| blink::mojom::AggregatableReportHistogramContribution( |
| /*bucket=*/123, |
| /*value=*/456, /*filtering_id=*/std::nullopt), |
| blink::mojom::AggregatableReportHistogramContribution( |
| /*bucket=*/7890, |
| /*value=*/1234, /*filtering_id=*/std::nullopt)}; |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create(expected_payload_contents, |
| example_request.shared_info().Clone()); |
| ASSERT_TRUE(request.has_value()); |
| |
| AggregatableReportSharedInfo expected_shared_info = |
| request->shared_info().Clone(); |
| |
| aggregation_service::TestHpkeKey hpke_key("id123"); |
| |
| std::optional<AggregatableReport> report = |
| AggregatableReport::Provider().CreateFromRequestAndPublicKey( |
| std::move(*request), hpke_key.GetPublicKey()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| VerifyReport(report, expected_payload_contents, expected_shared_info, |
| /*expected_debug_key=*/std::nullopt, |
| /*expected_additional_fields=*/{}, hpke_key)); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| ValidNoContributionsRequest_ValidReportReturned) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.contributions.clear(); |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create(payload_contents, |
| example_request.shared_info().Clone()); |
| ASSERT_TRUE(request.has_value()); |
| |
| AggregatableReportSharedInfo expected_shared_info = |
| request->shared_info().Clone(); |
| |
| aggregation_service::TestHpkeKey hpke_key("id123"); |
| |
| std::optional<AggregatableReport> report = |
| AggregatableReport::Provider().CreateFromRequestAndPublicKey( |
| std::move(*request), hpke_key.GetPublicKey()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| VerifyReport(report, /*expected_payload_contents=*/payload_contents, |
| expected_shared_info, |
| /*expected_debug_key=*/std::nullopt, |
| /*expected_additional_fields=*/{}, hpke_key)); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| ValidDebugModeEnabledRequest_ValidReportReturned) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo expected_shared_info = |
| example_request.shared_info().Clone(); |
| expected_shared_info.debug_mode = |
| AggregatableReportSharedInfo::DebugMode::kEnabled; |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create(example_request.payload_contents(), |
| expected_shared_info.Clone()); |
| ASSERT_TRUE(request.has_value()); |
| |
| AggregationServicePayloadContents expected_payload_contents = |
| request->payload_contents(); |
| |
| aggregation_service::TestHpkeKey hpke_key("id123"); |
| |
| std::optional<AggregatableReport> report = |
| AggregatableReport::Provider().CreateFromRequestAndPublicKey( |
| std::move(request.value()), hpke_key.GetPublicKey()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| VerifyReport(report, expected_payload_contents, expected_shared_info, |
| /*expected_debug_key=*/std::nullopt, |
| /*expected_additional_fields=*/{}, hpke_key)); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| ValidDebugKeyPresentRequest_ValidReportReturned) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo expected_shared_info = |
| example_request.shared_info().Clone(); |
| expected_shared_info.debug_mode = |
| AggregatableReportSharedInfo::DebugMode::kEnabled; |
| |
| // Use a large value to check that higher order bits are serialized too. |
| uint64_t expected_debug_key = std::numeric_limits<uint64_t>::max() - 1; |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), expected_shared_info.Clone(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| /*reporting_path=*/std::string(), expected_debug_key); |
| ASSERT_TRUE(request.has_value()); |
| |
| AggregationServicePayloadContents expected_payload_contents = |
| request->payload_contents(); |
| |
| aggregation_service::TestHpkeKey hpke_key("id123"); |
| |
| std::optional<AggregatableReport> report = |
| AggregatableReport::Provider().CreateFromRequestAndPublicKey( |
| std::move(request.value()), hpke_key.GetPublicKey()); |
| |
| ASSERT_NO_FATAL_FAILURE(VerifyReport(report, expected_payload_contents, |
| expected_shared_info, expected_debug_key, |
| /*expected_additional_fields=*/{}, |
| hpke_key)); |
| } |
| |
| TEST_F(AggregatableReportTest, AdditionalFieldsPresent_ValidReportReturned) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| base::flat_map<std::string, std::string> expected_additional_fields = { |
| {"additional_key", "example_value"}, {"second", "field"}, {"", ""}}; |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), |
| example_request.shared_info().Clone(), |
| AggregatableReportRequest::DelayType::Unscheduled, |
| /*reporting_path=*/std::string(), |
| /*debug_key=*/std::nullopt, expected_additional_fields); |
| ASSERT_TRUE(request.has_value()); |
| |
| AggregationServicePayloadContents expected_payload_contents = |
| request->payload_contents(); |
| |
| aggregation_service::TestHpkeKey hpke_key("id123"); |
| |
| std::optional<AggregatableReport> report = |
| AggregatableReport::Provider().CreateFromRequestAndPublicKey( |
| std::move(request.value()), hpke_key.GetPublicKey()); |
| |
| ASSERT_NO_FATAL_FAILURE(VerifyReport(report, expected_payload_contents, |
| example_request.shared_info(), |
| /*expected_debug_key=*/std::nullopt, |
| expected_additional_fields, hpke_key)); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| FilteringIdMaxBytesSpecified_ValidReportReturned) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.filtering_id_max_bytes = 1; |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create(payload_contents, |
| example_request.shared_info().Clone()); |
| ASSERT_TRUE(request.has_value()); |
| |
| aggregation_service::TestHpkeKey hpke_key("id123"); |
| |
| std::optional<AggregatableReport> report = |
| AggregatableReport::Provider().CreateFromRequestAndPublicKey( |
| std::move(request.value()), hpke_key.GetPublicKey()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| VerifyReport(report, payload_contents, example_request.shared_info(), |
| /*expected_debug_key=*/std::nullopt, |
| /*expected_additional_fields=*/{}, hpke_key)); |
| } |
| |
| TEST_F(AggregatableReportTest, FilteringIdsSpecified_ValidReportReturned) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.contributions.clear(); |
| payload_contents.contributions.emplace_back(/*bucket=*/123, /*value=*/456, |
| /*filtering_id=*/std::nullopt); |
| payload_contents.contributions.emplace_back(/*bucket=*/234, /*value=*/567, |
| /*filtering_id=*/0); |
| payload_contents.contributions.emplace_back( |
| /*bucket=*/345, /*value=*/678, /*filtering_id=*/(1ULL << (5 * 8)) - 1); |
| |
| payload_contents.filtering_id_max_bytes = 5; |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create(payload_contents, |
| example_request.shared_info().Clone()); |
| ASSERT_TRUE(request.has_value()); |
| |
| aggregation_service::TestHpkeKey hpke_key("id123"); |
| |
| std::optional<AggregatableReport> report = |
| AggregatableReport::Provider().CreateFromRequestAndPublicKey( |
| std::move(request.value()), hpke_key.GetPublicKey()); |
| |
| ASSERT_NO_FATAL_FAILURE( |
| VerifyReport(report, payload_contents, example_request.shared_info(), |
| /*expected_debug_key=*/std::nullopt, |
| /*expected_additional_fields=*/{}, hpke_key)); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| RequestCreatedWithNonPositiveValue_FailsIfNegative) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| |
| AggregationServicePayloadContents zero_value_payload_contents = |
| payload_contents; |
| zero_value_payload_contents.contributions[0].value = 0; |
| std::optional<AggregatableReportRequest> zero_value_request = |
| AggregatableReportRequest::Create(zero_value_payload_contents, |
| shared_info.Clone()); |
| EXPECT_TRUE(zero_value_request.has_value()); |
| |
| AggregationServicePayloadContents negative_value_payload_contents = |
| payload_contents; |
| negative_value_payload_contents.contributions[0].value = -1; |
| std::optional<AggregatableReportRequest> negative_value_request = |
| AggregatableReportRequest::Create(negative_value_payload_contents, |
| shared_info.Clone()); |
| EXPECT_FALSE(negative_value_request.has_value()); |
| } |
| |
| TEST_F(AggregatableReportTest, RequestCreatedWithInvalidReportId_Failed) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| shared_info.report_id = base::Uuid(); |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create(example_request.payload_contents(), |
| std::move(shared_info)); |
| |
| EXPECT_FALSE(request.has_value()); |
| } |
| |
| TEST_F(AggregatableReportTest, RequestCreatedWithZeroContributions) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| |
| payload_contents.contributions.clear(); |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create(payload_contents, |
| example_request.shared_info().Clone()); |
| EXPECT_TRUE(request.has_value()); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| RequestCreatedWithDebugKeyButDebugModeDisabled_Failed) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), |
| example_request.shared_info().Clone(), |
| AggregatableReportRequest::DelayType::Unscheduled, |
| /*reporting_path=*/std::string(), |
| /*debug_key=*/1234); |
| |
| EXPECT_FALSE(request.has_value()); |
| } |
| |
| TEST_F(AggregatableReportTest, GetAsJsonOnePayload_ValidJsonReturned) { |
| AggregatableReport report(AggregatableReport::AggregationServicePayload( |
| /*payload=*/kABCD1234AsBytes, |
| /*key_id=*/"key_1", |
| /*debug_cleartext_payload=*/std::nullopt), |
| "example_shared_info", |
| /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{}, |
| /*aggregation_coordinator_origin=*/std::nullopt); |
| |
| std::string report_json_string; |
| base::JSONWriter::Write(base::Value(report.GetAsJson()), &report_json_string); |
| |
| const char kExpectedJsonString[] = |
| R"({)" |
| R"("aggregation_coordinator_origin":"https://a.test",)" |
| R"("aggregation_service_payloads":[)" |
| R"({"key_id":"key_1","payload":"ABCD1234"})" |
| R"(],)" |
| R"("shared_info":"example_shared_info")" |
| R"(})"; |
| EXPECT_EQ(report_json_string, kExpectedJsonString); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| GetAsJsonDebugCleartextPayload_ValidJsonReturned) { |
| AggregatableReport report(AggregatableReport::AggregationServicePayload( |
| /*payload=*/kABCD1234AsBytes, |
| /*key_id=*/"key_1", |
| /*debug_cleartext_payload=*/kEFGH5678AsBytes), |
| "example_shared_info", |
| /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{}, |
| /*aggregation_coordinator_origin=*/std::nullopt); |
| |
| std::string report_json_string; |
| base::JSONWriter::Write(base::Value(report.GetAsJson()), &report_json_string); |
| |
| const char kExpectedJsonString[] = |
| R"({)" |
| R"("aggregation_coordinator_origin":"https://a.test",)" |
| R"("aggregation_service_payloads":[{)" |
| R"("debug_cleartext_payload":"EFGH5678",)" |
| R"("key_id":"key_1",)" |
| R"("payload":"ABCD1234")" |
| R"(}],)" |
| R"("shared_info":"example_shared_info")" |
| R"(})"; |
| EXPECT_EQ(report_json_string, kExpectedJsonString); |
| } |
| |
| TEST_F(AggregatableReportTest, GetAsJsonDebugKey_ValidJsonReturned) { |
| AggregatableReport report(AggregatableReport::AggregationServicePayload( |
| /*payload=*/kABCD1234AsBytes, |
| /*key_id=*/"key_1", |
| /*debug_cleartext_payload=*/kEFGH5678AsBytes), |
| "example_shared_info", |
| /*debug_key=*/1234, /*additional_fields=*/{}, |
| /*aggregation_coordinator_origin=*/std::nullopt); |
| |
| std::string report_json_string; |
| base::JSONWriter::Write(base::Value(report.GetAsJson()), &report_json_string); |
| |
| const char kExpectedJsonString[] = |
| R"({)" |
| R"("aggregation_coordinator_origin":"https://a.test",)" |
| R"("aggregation_service_payloads":[{)" |
| R"("debug_cleartext_payload":"EFGH5678",)" |
| R"("key_id":"key_1",)" |
| R"("payload":"ABCD1234")" |
| R"(}],)" |
| R"("debug_key":"1234",)" |
| R"("shared_info":"example_shared_info")" |
| R"(})"; |
| EXPECT_EQ(report_json_string, kExpectedJsonString); |
| } |
| |
| TEST_F(AggregatableReportTest, GetAsJsonAdditionalFields_ValidJsonReturned) { |
| AggregatableReport report( |
| AggregatableReport::AggregationServicePayload( |
| /*payload=*/kABCD1234AsBytes, |
| /*key_id=*/"key_1", |
| /*debug_cleartext_payload=*/std::nullopt), |
| "example_shared_info", |
| /*debug_key=*/std::nullopt, /*additional_fields=*/ |
| {{"additional_key", "example_value"}, {"second", "field"}, {"", ""}}, |
| /*aggregation_coordinator_origin=*/std::nullopt); |
| |
| std::string report_json_string; |
| base::JSONWriter::Write(base::Value(report.GetAsJson()), &report_json_string); |
| |
| const char kExpectedJsonString[] = |
| R"({)" |
| R"("":"",)" |
| R"("additional_key":"example_value",)" |
| R"("aggregation_coordinator_origin":"https://a.test",)" |
| R"("aggregation_service_payloads":[{)" |
| R"("key_id":"key_1",)" |
| R"("payload":"ABCD1234")" |
| R"(}],)" |
| R"("second":"field",)" |
| R"("shared_info":"example_shared_info")" |
| R"(})"; |
| EXPECT_EQ(report_json_string, kExpectedJsonString); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| SharedInfoDebugModeDisabled_SerializeAsJsonReturnsExpectedString) { |
| AggregatableReportSharedInfo shared_info( |
| base::Time::FromMillisecondsSinceUnixEpoch(1234567890123), |
| /*report_id=*/ |
| base::Uuid::ParseLowercase("21abd97f-73e8-4b88-9389-a9fee6abda5e"), |
| url::Origin::Create(GURL("https://reporting.example")), |
| AggregatableReportSharedInfo::DebugMode::kDisabled, base::Value::Dict(), |
| /*api_version=*/"1.0", |
| /*api_identifier=*/"example-api"); |
| |
| const char kExpectedString[] = |
| R"({)" |
| R"("api":"example-api",)" |
| R"("report_id":"21abd97f-73e8-4b88-9389-a9fee6abda5e",)" |
| R"("reporting_origin":"https://reporting.example",)" |
| R"("scheduled_report_time":"1234567890",)" |
| R"("version":"1.0")" |
| R"(})"; |
| |
| EXPECT_EQ(shared_info.SerializeAsJson(), kExpectedString); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| SharedInfoDebugModeEnabled_SerializeAsJsonReturnsExpectedString) { |
| AggregatableReportSharedInfo shared_info( |
| base::Time::FromMillisecondsSinceUnixEpoch(1234567890123), |
| /*report_id=*/ |
| base::Uuid::ParseLowercase("21abd97f-73e8-4b88-9389-a9fee6abda5e"), |
| url::Origin::Create(GURL("https://reporting.example")), |
| AggregatableReportSharedInfo::DebugMode::kEnabled, base::Value::Dict(), |
| /*api_version=*/"1.0", |
| /*api_identifier=*/"example-api"); |
| |
| const char kExpectedString[] = |
| R"({)" |
| R"("api":"example-api",)" |
| R"("debug_mode":"enabled",)" |
| R"("report_id":"21abd97f-73e8-4b88-9389-a9fee6abda5e",)" |
| R"("reporting_origin":"https://reporting.example",)" |
| R"("scheduled_report_time":"1234567890",)" |
| R"("version":"1.0")" |
| R"(})"; |
| |
| EXPECT_EQ(shared_info.SerializeAsJson(), kExpectedString); |
| } |
| |
| TEST_F(AggregatableReportTest, SharedInfoAdditionalFields) { |
| base::Value::Dict additional_fields; |
| additional_fields.Set("foo", "1"); |
| additional_fields.Set("bar", "2"); |
| additional_fields.Set("baz", "3"); |
| AggregatableReportSharedInfo shared_info( |
| base::Time::FromMillisecondsSinceUnixEpoch(1234567890123), |
| /*report_id=*/ |
| base::Uuid::ParseLowercase("21abd97f-73e8-4b88-9389-a9fee6abda5e"), |
| url::Origin::Create(GURL("https://reporting.example")), |
| AggregatableReportSharedInfo::DebugMode::kEnabled, |
| std::move(additional_fields), |
| /*api_version=*/"1.0", |
| /*api_identifier=*/"example-api"); |
| |
| const char kExpectedString[] = |
| R"({)" |
| R"("api":"example-api",)" |
| R"("bar":"2",)" |
| R"("baz":"3",)" |
| R"("debug_mode":"enabled",)" |
| R"("foo":"1",)" |
| R"("report_id":"21abd97f-73e8-4b88-9389-a9fee6abda5e",)" |
| R"("reporting_origin":"https://reporting.example",)" |
| R"("scheduled_report_time":"1234567890",)" |
| R"("version":"1.0")" |
| R"(})"; |
| |
| EXPECT_EQ(shared_info.SerializeAsJson(), kExpectedString); |
| } |
| |
| TEST_F(AggregatableReportTest, ReportingPathSet_SetInRequest) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| std::string reporting_path = "/example-path"; |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), |
| example_request.shared_info().Clone(), |
| AggregatableReportRequest::DelayType::ScheduledWithReducedDelay, |
| reporting_path); |
| ASSERT_TRUE(request.has_value()); |
| EXPECT_EQ(request->reporting_path(), reporting_path); |
| EXPECT_EQ(request->GetReportingUrl().path(), reporting_path); |
| EXPECT_EQ(request->GetReportingUrl().GetWithEmptyPath(), |
| example_request.shared_info().reporting_origin.GetURL()); |
| } |
| |
| TEST_F(AggregatableReportTest, RequestCreatedWithInvalidFailedAttempt_Failed) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| AggregatableReportSharedInfo shared_info = |
| example_request.shared_info().Clone(); |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create( |
| example_request.payload_contents(), std::move(shared_info), |
| AggregatableReportRequest::DelayType::Unscheduled, |
| /*reporting_path=*/"", /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{}, |
| /*failed_send_attempts=*/-1); |
| |
| EXPECT_FALSE(request.has_value()); |
| } |
| |
| TEST_F(AggregatableReportTest, |
| RequestCreatedWithMaxContributionsAllowed_FailsIfInvalid) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| |
| payload_contents.contributions.emplace_back(/*bucket=*/456, |
| /*value=*/78, |
| /*filtering_id=*/std::nullopt); |
| payload_contents.max_contributions_allowed = 1; |
| |
| std::optional<AggregatableReportRequest> too_small_max_request = |
| AggregatableReportRequest::Create(payload_contents, |
| example_request.shared_info().Clone()); |
| EXPECT_FALSE(too_small_max_request.has_value()); |
| |
| payload_contents.contributions = {}; |
| payload_contents.max_contributions_allowed = 0; |
| |
| std::optional<AggregatableReportRequest> empty_zero_request = |
| AggregatableReportRequest::Create(payload_contents, |
| example_request.shared_info().Clone()); |
| EXPECT_TRUE(empty_zero_request.has_value()); |
| } |
| |
| TEST_F(AggregatableReportTest, FailedSendAttempts) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| // Requests are initialized with no failed attempts by default |
| EXPECT_EQ(example_request.failed_send_attempts(), 0); |
| |
| AggregatableReportRequest example_request_with_failed_attempts = |
| aggregation_service::CreateExampleRequest( |
| /*failed_send_attempts=*/2, |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| /*delay_type=*/ |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay); |
| |
| // The failed attempts are correctly serialized & deserialized |
| std::vector<uint8_t> proto = example_request_with_failed_attempts.Serialize(); |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize(proto); |
| EXPECT_THAT( |
| parsed_request, |
| testing::Optional(testing::AllOf( |
| testing::Property(&AggregatableReportRequest::failed_send_attempts, |
| 2), |
| testing::Property( |
| &AggregatableReportRequest::delay_type, |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay)))); |
| } |
| |
| TEST_F(AggregatableReportTest, DelayTypeSerializeNulloptCrashes) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest( |
| /*failed_send_attempts=*/0, |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| /*delay_type=*/std::nullopt); |
| |
| EXPECT_FALSE(example_request.delay_type().has_value()); |
| |
| EXPECT_CHECK_DEATH_WITH( |
| example_request.Serialize(), |
| "Check failed: request\\.delay_type\\(\\)\\.has_value\\(\\)"); |
| } |
| |
| TEST_F(AggregatableReportTest, DelayTypeSerializeUnscheduledCrashes) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest( |
| /*failed_send_attempts=*/0, |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| /*delay_type=*/AggregatableReportRequest::DelayType::Unscheduled); |
| |
| EXPECT_THAT( |
| example_request.delay_type(), |
| testing::Optional(AggregatableReportRequest::DelayType::Unscheduled)); |
| |
| EXPECT_CHECK_DEATH_WITH(example_request.Serialize(), |
| "Check failed: .*DelayType_IsValid"); |
| } |
| |
| TEST_F(AggregatableReportTest, DelayTypeSerializesAndDeserializesCorrectly) { |
| static const AggregatableReportRequest::DelayType kDelayTypeValues[] = { |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay, |
| AggregatableReportRequest::DelayType::ScheduledWithReducedDelay, |
| }; |
| for (const auto delay_type : kDelayTypeValues) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest( |
| /*failed_send_attempts=*/0, |
| /*aggregation_coordinator_origin=*/std::nullopt, delay_type); |
| |
| EXPECT_EQ(example_request.delay_type(), delay_type); |
| |
| // The delay_type field is correctly serialized & deserialized. |
| std::vector<uint8_t> proto = example_request.Serialize(); |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize(proto); |
| |
| ASSERT_TRUE(parsed_request.has_value()); |
| |
| EXPECT_THAT(parsed_request, |
| testing::Optional(testing::Property( |
| &AggregatableReportRequest::delay_type, delay_type))); |
| } |
| } |
| |
| TEST_F(AggregatableReportTest, MaxContributionsAllowed) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.max_contributions_allowed = 20; |
| |
| AggregatableReportRequest request = |
| AggregatableReportRequest::Create( |
| payload_contents, example_request.shared_info().Clone(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay) |
| .value(); |
| |
| // The max contributions allowed is correctly serialized and deserialized |
| std::vector<uint8_t> proto = request.Serialize(); |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize(proto); |
| ASSERT_TRUE(parsed_request.has_value()); |
| EXPECT_EQ(parsed_request->payload_contents().max_contributions_allowed, 20u); |
| EXPECT_EQ(parsed_request->delay_type(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay); |
| } |
| |
| TEST_F(AggregatableReportTest, AggregationCoordinatorOrigin) { |
| const struct { |
| std::optional<url::Origin> aggregation_coordinator_origin; |
| bool creation_should_succeed; |
| const char* description; |
| } kTestCases[] = { |
| {std::nullopt, true, "default coordinator"}, |
| {url::Origin::Create(GURL("https://a.test")), true, "valid coordinator"}, |
| {url::Origin::Create(GURL("https://c.test")), false, |
| "invalid coordinator"}, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| SCOPED_TRACE(test_case.description); |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.aggregation_coordinator_origin = |
| test_case.aggregation_coordinator_origin; |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create( |
| payload_contents, example_request.shared_info().Clone(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay); |
| |
| EXPECT_EQ(request.has_value(), test_case.creation_should_succeed); |
| |
| if (!request.has_value()) { |
| continue; |
| } |
| |
| // The coordinator origin is correctly serialized and deserialized |
| std::vector<uint8_t> proto = request->Serialize(); |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize(proto); |
| ASSERT_TRUE(parsed_request.has_value()); |
| EXPECT_EQ(parsed_request->payload_contents().aggregation_coordinator_origin, |
| test_case.aggregation_coordinator_origin); |
| EXPECT_EQ(parsed_request->delay_type(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay); |
| } |
| } |
| |
| TEST_F(AggregatableReportTest, AggregationCoordinatorOriginAllowlistChanged) { |
| std::optional< |
| ::aggregation_service::ScopedAggregationCoordinatorAllowlistForTesting> |
| scoped_coordinator_allowlist; |
| |
| scoped_coordinator_allowlist.emplace( |
| {url::Origin::Create(GURL("https://a.test"))}); |
| |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.aggregation_coordinator_origin = |
| url::Origin::Create(GURL("https://a.test")); |
| |
| AggregatableReportRequest request = |
| AggregatableReportRequest::Create( |
| payload_contents, example_request.shared_info().Clone(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay) |
| .value(); |
| |
| std::vector<uint8_t> proto = request.Serialize(); |
| |
| // Change the allowlist between serializing and deserializing |
| scoped_coordinator_allowlist.reset(); |
| scoped_coordinator_allowlist.emplace( |
| {url::Origin::Create(GURL("https://b.test"))}); |
| |
| // Expect the report to fail to be recreated. |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize(proto); |
| EXPECT_FALSE(parsed_request.has_value()); |
| } |
| |
| TEST_F(AggregatableReportTest, ReportingPathEmpty_NotSetInRequest) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create(example_request.payload_contents(), |
| example_request.shared_info().Clone()); |
| ASSERT_TRUE(request.has_value()); |
| EXPECT_TRUE(request->reporting_path().empty()); |
| |
| // If the reporting path is empty, |
| EXPECT_FALSE(request->GetReportingUrl().is_valid()); |
| } |
| |
| TEST_F(AggregatableReportTest, EmptyPayloads) { |
| AggregatableReport report(/*payloads=*/{}, "example_shared_info", |
| /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{}, |
| /*aggregation_coordinator_origin=*/std::nullopt); |
| |
| std::string report_json_string; |
| base::JSONWriter::Write(base::Value(report.GetAsJson()), &report_json_string); |
| |
| const char kExpectedJsonString[] = |
| R"({)" |
| R"("aggregation_coordinator_origin":"https://a.test",)" |
| R"("shared_info":"example_shared_info")" |
| R"(})"; |
| EXPECT_EQ(report_json_string, kExpectedJsonString); |
| } |
| |
| TEST_F(AggregatableReportTest, FilteringIdMaxBytesMax) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.filtering_id_max_bytes = |
| AggregationServicePayloadContents::kMaximumFilteringIdMaxBytes; |
| |
| // Trying to set any explicit filtering ID (or none) should work. |
| const std::optional<uint64_t> kTestCases[] = { |
| std::nullopt, 0, 1, std::numeric_limits<uint64_t>::max()}; |
| |
| for (const std::optional<uint64_t> test_case : kTestCases) { |
| payload_contents.contributions[0].filtering_id = test_case; |
| |
| AggregatableReportRequest request = |
| AggregatableReportRequest::Create( |
| payload_contents, example_request.shared_info().Clone(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay) |
| .value(); |
| |
| // The report is correctly serialized and deserialized |
| std::vector<uint8_t> proto = request.Serialize(); |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize(proto); |
| EXPECT_EQ(parsed_request.value().payload_contents().filtering_id_max_bytes, |
| AggregationServicePayloadContents::kMaximumFilteringIdMaxBytes); |
| EXPECT_EQ( |
| parsed_request.value().payload_contents().contributions[0].filtering_id, |
| test_case); |
| } |
| } |
| |
| TEST_F(AggregatableReportTest, FilteringIdMaxBytesNotMax) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.filtering_id_max_bytes = 1; |
| |
| // The filtering ID needs to fit in the specified space to be accepted. |
| const struct { |
| std::optional<uint64_t> filtering_id; |
| bool expect_success; |
| } kTestCases[] = {{std::nullopt, true}, |
| {0, true}, |
| {1, true}, |
| {255, true}, |
| {256, false}, |
| {std::numeric_limits<uint64_t>::max(), false}}; |
| |
| for (const auto& test_case : kTestCases) { |
| payload_contents.contributions[0].filtering_id = test_case.filtering_id; |
| |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create( |
| payload_contents, example_request.shared_info().Clone(), |
| AggregatableReportRequest::DelayType::ScheduledWithFullDelay); |
| EXPECT_EQ(request.has_value(), test_case.expect_success); |
| |
| if (!request.has_value()) { |
| continue; |
| } |
| |
| // The report is correctly serialized and deserialized |
| std::vector<uint8_t> proto = request->Serialize(); |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize(proto); |
| EXPECT_EQ(parsed_request.value().payload_contents().filtering_id_max_bytes, |
| 1u); |
| EXPECT_EQ( |
| parsed_request.value().payload_contents().contributions[0].filtering_id, |
| test_case.filtering_id); |
| } |
| } |
| |
| TEST_F(AggregatableReportTest, FilteringIdMaxBytesTooSmall) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.filtering_id_max_bytes = 0; |
| EXPECT_FALSE(AggregatableReportRequest::Create( |
| payload_contents, example_request.shared_info().Clone()) |
| .has_value()); |
| |
| payload_contents.filtering_id_max_bytes = -1; |
| EXPECT_FALSE(AggregatableReportRequest::Create( |
| payload_contents, example_request.shared_info().Clone()) |
| .has_value()); |
| } |
| |
| TEST_F(AggregatableReportTest, FilteringIdMaxBytesTooLarge) { |
| AggregatableReportRequest example_request = |
| aggregation_service::CreateExampleRequest(); |
| |
| AggregationServicePayloadContents payload_contents = |
| example_request.payload_contents(); |
| payload_contents.filtering_id_max_bytes = |
| AggregationServicePayloadContents::kMaximumFilteringIdMaxBytes + 1; |
| EXPECT_FALSE(AggregatableReportRequest::Create( |
| payload_contents, example_request.shared_info().Clone()) |
| .has_value()); |
| } |
| |
| TEST(AggregatableReportProtoMigrationTest, |
| NoDebugKeyOrFailedSendAttempts_ParsesCorrectly) { |
| // An `AggregatableReport` serialized before the addition of the `debug_key` |
| // field and `failed_send_attempts` field. |
| const char kHexEncodedOldProto[] = |
| "0A071205107B18C803126208D0DA8693FDBECF17122431323334353637382D393061622D" |
| "346364652D386631322D3334353637383930616263641A1368747470733A2F2F6578616D" |
| "706C652E636F6D2A0F6578616D706C652D76657273696F6E320B6578616D706C652D6170" |
| "691A0C6578616D706C652D70617468"; |
| |
| std::vector<uint8_t> old_proto; |
| EXPECT_TRUE(base::HexStringToBytes(kHexEncodedOldProto, &old_proto)); |
| |
| std::optional<AggregatableReportRequest> deserialized_request = |
| AggregatableReportRequest::Deserialize(old_proto); |
| ASSERT_TRUE(deserialized_request.has_value()); |
| |
| 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=*/1u, |
| /*filtering_id_max_bytes=*/1u), // Default max bytes used. |
| AggregatableReportSharedInfo( |
| base::Time::FromMillisecondsSinceUnixEpoch(1652984901234), |
| base::Uuid::ParseLowercase( |
| "12345678-90ab-4cde-8f12-34567890abcd"), |
| /*reporting_origin=*/ |
| url::Origin::Create(GURL("https://example.com")), |
| AggregatableReportSharedInfo::DebugMode::kDisabled, |
| /*additional_fields=*/base::Value::Dict(), |
| /*api_version=*/"example-version", |
| /*api_identifier=*/"example-api"), |
| /*delay_type=*/std::nullopt, |
| /*reporting_path=*/"example-path", /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{}, |
| /*failed_send_attempts=*/0) |
| .value(); |
| |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| deserialized_request.value(), expected_request)); |
| } |
| |
| TEST(AggregatableReportProtoMigrationTest, NegativeDebugKey_ParsesCorrectly) { |
| // An `AggregatableReport` serialized while `debug_key` was stored as a signed |
| // int64 and used a value that was larger than the maximum int64. It was |
| // therefore stored as a negative number. |
| const char kHexEncodedOldProto[] = |
| "0A071205107B18C803126408D0DA8693FDBECF17122431323334353637382D393061622D" |
| "346364652D386631322D3334353637383930616263641A1368747470733A2F2F6578616D" |
| "706C652E636F6D20012A0F6578616D706C652D76657273696F6E320B6578616D706C652D" |
| "6170691A0C6578616D706C652D7061746820FFFFFFFFFFFFFFFFFF01"; |
| |
| std::vector<uint8_t> old_proto; |
| EXPECT_TRUE(base::HexStringToBytes(kHexEncodedOldProto, &old_proto)); |
| |
| std::optional<AggregatableReportRequest> deserialized_request = |
| AggregatableReportRequest::Deserialize(old_proto); |
| ASSERT_TRUE(deserialized_request.has_value()); |
| |
| 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=*/1u, |
| /*filtering_id_max_bytes=*/1u), // Default max bytes used. |
| AggregatableReportSharedInfo( |
| base::Time::FromMillisecondsSinceUnixEpoch(1652984901234), |
| base::Uuid::ParseLowercase( |
| "12345678-90ab-4cde-8f12-34567890abcd"), |
| /*reporting_origin=*/ |
| url::Origin::Create(GURL("https://example.com")), |
| AggregatableReportSharedInfo::DebugMode::kEnabled, |
| /*additional_fields=*/base::Value::Dict(), |
| /*api_version=*/"example-version", |
| /*api_identifier=*/"example-api"), |
| /*delay_type=*/std::nullopt, |
| /*reporting_path=*/"example-path", |
| /*debug_key=*/std::numeric_limits<uint64_t>::max()) |
| .value(); |
| |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| deserialized_request.value(), expected_request)); |
| } |
| |
| TEST( |
| AggregatableReportProtoMigrationTest, |
| NoAdditionalFieldsOrAggregationCoordinatorOriginOrFilteringIdOrDelayType_ParsesCorrectly) { |
| // An `AggregatableReport` serialized before `additional_fields`, |
| // `aggregation_coordinator_origin`, `filtering_id`, `filtering_id_max_bytes`, |
| // and `delay_type` were added to the proto definition. |
| const char kHexEncodedOldProto[] = |
| "0A071205107B18C803126208D0DA8693FDBECF17122431323334353637382D393061622D" |
| "346364652D386631322D3334353637383930616263641A1368747470733A2F2F6578616D" |
| "706C652E636F6D2A0F6578616D706C652D76657273696F6E320B6578616D706C652D6170" |
| "691A0C6578616D706C652D70617468"; |
| |
| std::vector<uint8_t> old_proto; |
| EXPECT_TRUE(base::HexStringToBytes(kHexEncodedOldProto, &old_proto)); |
| |
| std::optional<AggregatableReportRequest> deserialized_request = |
| AggregatableReportRequest::Deserialize(old_proto); |
| ASSERT_TRUE(deserialized_request.has_value()); |
| |
| 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=*/1u, |
| /*filtering_id_max_bytes=*/1u), // Default max bytes used. |
| AggregatableReportSharedInfo( |
| base::Time::FromMillisecondsSinceUnixEpoch(1652984901234), |
| base::Uuid::ParseLowercase( |
| "12345678-90ab-4cde-8f12-34567890abcd"), |
| /*reporting_origin=*/ |
| url::Origin::Create(GURL("https://example.com")), |
| AggregatableReportSharedInfo::DebugMode::kDisabled, |
| /*additional_fields=*/base::Value::Dict(), |
| /*api_version=*/"example-version", |
| /*api_identifier=*/"example-api"), |
| /*delay_type=*/std::nullopt, |
| /*reporting_path=*/"example-path", /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{}, |
| /*failed_send_attempts=*/0) |
| .value(); |
| |
| EXPECT_TRUE(aggregation_service::ReportRequestsEqual( |
| deserialized_request.value(), expected_request)); |
| } |
| |
| TEST_F(AggregatableReportTest, ProcessingUrlSet) { |
| AggregatableReportRequest request = |
| aggregation_service::CreateExampleRequest(); |
| EXPECT_EQ( |
| request.processing_url(), |
| GetAggregationServiceProcessingUrl( |
| ::aggregation_service::GetDefaultAggregationCoordinatorOrigin())); |
| } |
| |
| TEST_F(AggregatableReportTest, AggregationCoordinator_ProcessingUrlSet) { |
| const struct { |
| std::optional<url::Origin> aggregation_coordinator_origin; |
| std::optional<GURL> expected_url; |
| } kTestCases[] = { |
| { |
| std::nullopt, |
| GURL("https://a.test/.well-known/aggregation-service/v1/" |
| "public-keys"), |
| }, |
| { |
| url::Origin::Create(GURL("https://a.test")), |
| GURL("https://a.test/.well-known/aggregation-service/v1/" |
| "public-keys"), |
| }, |
| { |
| url::Origin::Create(GURL("https://b.test")), |
| GURL("https://b.test/.well-known/aggregation-service/v1/" |
| "public-keys"), |
| }, |
| { |
| url::Origin::Create(GURL("https://c.test")), |
| std::nullopt, |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| std::optional<AggregatableReportRequest> request = |
| AggregatableReportRequest::Create( |
| AggregationServicePayloadContents( |
| AggregationServicePayloadContents::Operation::kHistogram, |
| {blink::mojom::AggregatableReportHistogramContribution( |
| /*bucket=*/123, |
| /*value=*/456, |
| /*filtering_id=*/std::nullopt)}, |
| test_case.aggregation_coordinator_origin, |
| /*max_contributions_allowed=*/20u, |
| /*filtering_id_max_bytes=*/1u), // Default max bytes used. |
| AggregatableReportSharedInfo( |
| /*scheduled_report_time=*/base::Time::Now(), |
| /*report_id=*/ |
| base::Uuid::GenerateRandomV4(), |
| url::Origin::Create(GURL("https://reporting.example")), |
| AggregatableReportSharedInfo::DebugMode::kDisabled, |
| /*additional_fields=*/base::Value::Dict(), |
| /*api_version=*/"", |
| /*api_identifier=*/"example-api"), |
| AggregatableReportRequest::DelayType::Unscheduled, |
| /*reporting_path=*/"example-path", |
| /*debug_key=*/std::nullopt, /*additional_fields=*/{}, |
| /*failed_send_attempts=*/0); |
| |
| ASSERT_EQ(request.has_value(), test_case.expected_url.has_value()); |
| |
| if (test_case.expected_url.has_value()) { |
| EXPECT_EQ(request->processing_url(), *test_case.expected_url); |
| } |
| } |
| } |
| |
| TEST_F(AggregatableReportTest, AggregationCoordinator_SetInReport) { |
| AggregatableReport report(AggregatableReport::AggregationServicePayload( |
| /*payload=*/kABCD1234AsBytes, |
| /*key_id=*/"key_1", |
| /*debug_cleartext_payload=*/std::nullopt), |
| "example_shared_info", |
| /*debug_key=*/std::nullopt, |
| /*additional_fields=*/{}, |
| /*aggregation_coordinator_origin=*/std::nullopt); |
| |
| std::string report_json_string; |
| base::JSONWriter::Write(base::Value(report.GetAsJson()), &report_json_string); |
| |
| const char kExpectedJsonString[] = |
| R"({)" |
| R"("aggregation_coordinator_origin":"https://a.test",)" |
| R"("aggregation_service_payloads":[)" |
| R"({"key_id":"key_1","payload":"ABCD1234"})" |
| R"(],)" |
| R"("shared_info":"example_shared_info")" |
| R"(})"; |
| EXPECT_EQ(report_json_string, kExpectedJsonString); |
| } |
| |
| TEST(AggregatableReportPayloadLengthTest, With20Contributions) { |
| // NOTE: These expectations are inscrutable when they fail due to |
| // `StrictNumeric`, unless we include base/numerics/ostream_operators.h. |
| EXPECT_EQ(AggregatableReport::ComputePayloadLengthInBytesForTesting( |
| /*num_contributions=*/20u, /*filtering_id_max_bytes=*/1u), |
| 847); |
| } |
| |
| TEST(AggregatableReportPayloadLengthTest, With100Contributions) { |
| EXPECT_EQ(AggregatableReport::ComputePayloadLengthInBytesForTesting( |
| /*num_contributions=*/100u, /*filtering_id_max_bytes=*/1u), |
| 4128); |
| } |
| |
| TEST(AggregatableReportPayloadLengthTest, OutOfRange) { |
| EXPECT_FALSE(AggregatableReport::ComputePayloadLengthInBytesForTesting( |
| /*num_contributions=*/std::numeric_limits<size_t>::max(), |
| /*filtering_id_max_bytes=*/1u) |
| .has_value()); |
| if constexpr (std::numeric_limits<size_t>::max() > |
| std::numeric_limits<uint32_t>::max()) { |
| EXPECT_FALSE( |
| AggregatableReport::ComputePayloadLengthInBytesForTesting( |
| // It's critical to convert the max `uint32_t` value to size_t |
| // before adding one to avoid an unwanted integer overflow. |
| /*num_contributions=*/size_t{std::numeric_limits<uint32_t>::max()} + |
| 1, |
| /*filtering_id_max_bytes=*/1u) |
| .has_value()); |
| } |
| } |
| |
| TEST(AggregatableReportPayloadLengthTest, PredictionMatchesReality) { |
| constexpr size_t kNumContributionsValues[] = { |
| 20, 100, 1000, |
| // Numbers near the CBOR edge case of 0x17 elements in an array. |
| 0x16, 0x17, 0x18, |
| // Edge cases for one-byte and two-byte length prefixes. |
| 255, 256, 257, 65535, 65536, 65537}; |
| |
| constexpr size_t kFilteringIdMaxBytesValues[] = {1u, 2u, 3u, 4u, 8u}; |
| |
| for (const size_t num_contributions : kNumContributionsValues) { |
| for (const size_t filtering_id_max_bytes : kFilteringIdMaxBytesValues) { |
| SCOPED_TRACE(testing::Message() |
| << "num_contributions: " << num_contributions |
| << ", filtering_id_max_bytes: " << filtering_id_max_bytes); |
| |
| const std::optional<size_t> predicted_length = |
| AggregatableReport::ComputePayloadLengthInBytesForTesting( |
| num_contributions, filtering_id_max_bytes); |
| ASSERT_TRUE(predicted_length.has_value()); |
| |
| blink::mojom::AggregatableReportHistogramContribution contribution( |
| /*bucket=*/0, /*value=*/0, /*filtering_id=*/std::nullopt); |
| std::vector<blink::mojom::AggregatableReportHistogramContribution> |
| contributions(/*count=*/num_contributions, /*value=*/contribution); |
| AggregationServicePayloadContents payload_contents( |
| AggregationServicePayloadContents::Operation::kHistogram, |
| contributions, |
| /*aggregation_coordinator_origin=*/std::nullopt, |
| /*max_contributions_allowed=*/num_contributions, |
| filtering_id_max_bytes); |
| |
| const std::optional<std::vector<uint8_t>> serialized = |
| AggregatableReport::SerializePayloadForTesting(payload_contents); |
| ASSERT_TRUE(serialized.has_value()); |
| EXPECT_EQ(serialized->size(), *predicted_length); |
| } |
| } |
| } |
| |
| } // namespace |
| } // namespace content |