blob: 1897e8db70774b5c1640b125d0600a3b6086e04b [file] [log] [blame]
// Copyright 2024 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/interest_group/trusted_signals_fetcher.h"
#include <stdint.h>
#include <limits>
#include <list>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/lock.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/thread_annotations.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#include "components/cbor/writer.h"
#include "content/browser/interest_group/bidding_and_auction_server_key_fetcher.h"
#include "content/browser/interest_group/data_decoder_manager.h"
#include "content/public/browser/frame_tree_node_id.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test_utils.h"
#include "content/services/auction_worklet/public/cpp/auction_downloader.h"
#include "content/services/auction_worklet/public/cpp/cbor_test_util.h"
#include "content/services/auction_worklet/public/mojom/trusted_signals_cache.mojom.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/isolation_info.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/site_for_cookies.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/third_party/quiche/src/quiche/oblivious_http/common/oblivious_http_header_key_config.h"
#include "net/third_party/quiche/src/quiche/oblivious_http/oblivious_http_gateway.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/cpp/cross_origin_embedder_policy.h"
#include "services/network/public/cpp/document_isolation_policy.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/mojom/client_security_state.mojom.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "services/network/public/mojom/cross_origin_embedder_policy.mojom.h"
#include "services/network/public/mojom/document_isolation_policy.mojom.h"
#include "services/network/public/mojom/ip_address_space.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/test/test_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/boringssl/src/include/openssl/hpke.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
// These keys were randomly generated as follows:
// EVP_HPKE_KEY keys;
// EVP_HPKE_KEY_generate(&keys, EVP_hpke_x25519_hkdf_sha256());
// and then EVP_HPKE_KEY_public_key and EVP_HPKE_KEY_private_key were used to
// extract the keys.
const uint8_t kTestPrivateKey[] = {
0xff, 0x1f, 0x47, 0xb1, 0x68, 0xb6, 0xb9, 0xea, 0x65, 0xf7, 0x97,
0x4f, 0xf2, 0x2e, 0xf2, 0x36, 0x94, 0xe2, 0xf6, 0xb6, 0x8d, 0x66,
0xf3, 0xa7, 0x64, 0x14, 0x28, 0xd4, 0x45, 0x35, 0x01, 0x8f,
};
const uint8_t kTestPublicKey[] = {
0xa1, 0x5f, 0x40, 0x65, 0x86, 0xfa, 0xc4, 0x7b, 0x99, 0x59, 0x70,
0xf1, 0x85, 0xd9, 0xd8, 0x91, 0xc7, 0x4d, 0xcf, 0x1e, 0xb9, 0x1a,
0x7d, 0x50, 0xa5, 0x8b, 0x01, 0x68, 0x3e, 0x60, 0x05, 0x2d,
};
const uint8_t kKeyId = 3;
const char kKeyIdStr[] = "03";
// Helper to create a CompressionGroupResult given all field values.
// `compression_group_data` is a string that will be CBOR encoded to form the
// expected compression group body.
TrustedSignalsFetcher::CompressionGroupResult CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme compression_scheme,
std::string_view compression_group_data,
base::TimeDelta ttl) {
TrustedSignalsFetcher::CompressionGroupResult out;
out.compression_scheme = compression_scheme;
std::optional<std::vector<uint8_t>> content_string =
cbor::Writer::Write(cbor::Value(compression_group_data));
CHECK(content_string);
out.compression_group_data = std::move(content_string).value();
out.ttl = ttl;
return out;
}
// Shared test fixture for bidding and scoring signals. Note that scoring
// signals tests focus on request body generation, with little coverage of
// response parsing, since that path is identical for bidding and scoring
// signals.
class TrustedSignalsFetcherTest : public testing::Test {
public:
// This is the expected request body that corresponds to the request returned
// by CreateBasicBiddingSignalsRequest(). Stored as a raw hex string to
// provide better coverage of padding logic than using
// CreateKVv2RequestBody(), which uses the same padding code as the fetcher.
// It is the deterministic CBOR representation of the following, with a prefix
// and padding added:
// {
// "acceptCompression": [ "none", "gzip" ],
// "metadata": { "hostname": "host.test" },
// "partitions": [
// {
// "compressionGroupId": 0,
// "id": 0,
// "arguments": [
// {
// "tags": [ "interestGroupNames" ],
// "data": [ "group1" ]
// },
// {
// "tags": [ "keys" ],
// "data": [ "key1" ]
// }
// ]
// }
// ]
// }
const std::string_view kBasicBiddingSignalsRequestBody =
"00000000A9A3686D65746164617461A168686F73746E616D6569686F73742E746573746A"
"706172746974696F6E7381A36269640069617267756D656E747382A26464617461816667"
"726F75703164746167738172696E74657265737447726F75704E616D6573A26464617461"
"81646B657931647461677381646B65797372636F6D7072657373696F6E47726F75704964"
"0071616363657074436F6D7072657373696F6E82646E6F6E6564677A6970000000000000"
"000000000000000000000000000000000000000000";
// This is the expected request body that corresponds to the request returned
// by CreateBasicScoringSignalsRequest(). Stored as a raw hex string to
// provide better coverage of padding logic than using
// CreateKVv2RequestBody(), which uses the same padding code as the fetcher.
// It is the deterministic CBOR representation of the following, with a prefix
// and padding added:
// {
// "acceptCompression": [ "none", "gzip" ],
// "metadata": { "hostname": "host.test" },
// "partitions": [
// {
// "compressionGroupId": 0,
// "id": 0,
// "arguments": [
// {
// "tags": [ "renderURLs" ],
// "data": [ "https://render_url.test/foo" ]
// }
// ]
// }
// ]
// }
const std::string_view kBasicScoringSignalsRequestBody =
"00000000A0A3686D65746164617461A168686F73746E616D6569686F73742E746573746A"
"706172746974696F6E7381A36269640069617267756D656E747381A2646461746181781B"
"68747470733A2F2F72656E6465725F75726C2E746573742F666F6F6474616773816A7265"
"6E64657255524C7372636F6D7072657373696F6E47726F75704964007161636365707443"
"6F6D7072657373696F6E82646E6F6E6564677A6970000000000000000000000000000000"
"000000000000000000000000000000000000000000";
TrustedSignalsFetcherTest() {
base::FieldTrialParams lna_checks_params;
lna_checks_params["LocalNetworkAccessChecksWarn"] = "false";
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{network::features::kLocalNetworkAccessChecks, lna_checks_params},
// Enable `kProtectedAudienceCorsSafelistKVv2Signals` by default, so
// behavior matches the eventual expected behavior.
{network::features::kProtectedAudienceCorsSafelistKVv2Signals, {}}},
/*disabled_features=*/{});
embedded_test_server_.SetSSLConfig(
net::EmbeddedTestServer::CERT_TEST_NAMES);
embedded_test_server_.AddDefaultHandlers();
embedded_test_server_.RegisterRequestHandler(
base::BindRepeating(&TrustedSignalsFetcherTest::HandleSignalsRequest,
base::Unretained(this)));
EXPECT_TRUE(embedded_test_server_.Start());
SetResponseBodyAndAddHeader(DefaultResponseBody());
base::AutoLock auto_lock(lock_);
script_origin_ = embedded_test_server_.GetOrigin(kTrustedSignalsHost);
}
~TrustedSignalsFetcherTest() override {
base::AutoLock auto_lock(lock_);
// Any request body should have been verified.
EXPECT_FALSE(request_path_.has_value());
EXPECT_FALSE(request_body_.has_value());
}
// CBOR representation of a response with a single compression group. Same for
// both bidding and scoring signals.
static std::string DefaultResponseBody() {
return auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"ttlMs" : 100,
"content" : "compression group content"
}
]
})");
}
// Sets `script_origin_` to be cross origin to be cross-origin to the trusted
// signals URL. Additional, sets whether a CORS preflight request is expected
// to be observed, which should depend on whether the
// `kProtectedAudienceCorsSafelistKVv2Signals` Feature is enabled.
void SetCrossOrigin(bool cors_preflight_expected = false) {
base::AutoLock auto_lock(lock_);
// No requests are made to this origin, so doesn't need to come from the
// EmbeddedTestServer.
script_origin_ = url::Origin::Create(GURL("https://other-origin.test/"));
script_origin_is_same_origin_ = false;
cors_preflight_expected_ = cors_preflight_expected;
}
url::Origin GetScriptOrigin() {
base::AutoLock auto_lock(lock_);
return script_origin_;
}
GURL TrustedBiddingSignalsUrl() const {
return embedded_test_server_.GetURL(kTrustedSignalsHost,
kTrustedBiddingSignalsPath);
}
GURL TrustedScoringSignalsUrl() const {
return embedded_test_server_.GetURL(kTrustedSignalsHost,
kTrustedScoringSignalsPath);
}
// Creates a simple request with one compression group with a single
// partition with only one key, and no other optional parameters.
std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>
CreateBasicBiddingSignalsRequest() {
std::vector<TrustedSignalsFetcher::BiddingPartition> bidding_partitions;
bidding_partitions.emplace_back(
/*partition_id=*/0, &kDefaultInterestGroupNames, &kDefaultKeys,
&kDefaultAdditionalParams, /*buyer_tkv_signals=*/nullptr);
std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>
bidding_signals_request;
bidding_signals_request.emplace(0, std::move(bidding_partitions));
return bidding_signals_request;
}
// Creates a simple request with one compression group with a single
// partition with only a render URL.
std::map<int, std::vector<TrustedSignalsFetcher::ScoringPartition>>
CreateBasicScoringSignalsRequest() {
std::vector<TrustedSignalsFetcher::ScoringPartition> scoring_partitions;
scoring_partitions.emplace_back(
/*partition_id=*/0, &kDefaultRenderUrl, &kDefaultAdComponentRenderUrls,
&kDefaultAdditionalParams, /*seller_tkv_signals=*/nullptr);
std::map<int, std::vector<TrustedSignalsFetcher::ScoringPartition>>
scoring_signals_request;
scoring_signals_request.emplace(0, std::move(scoring_partitions));
return scoring_signals_request;
}
TrustedSignalsFetcher::SignalsFetchResult
RequestBiddingSignalsAndWaitForResult(
const std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>&
compression_groups,
std::optional<GURL> signals_url = std::nullopt) {
GURL url = signals_url.value_or(TrustedBiddingSignalsUrl());
base::RunLoop run_loop;
TrustedSignalsFetcher::SignalsFetchResult out;
TrustedSignalsFetcher trusted_signals_fetcher;
trusted_signals_fetcher.FetchBiddingSignals(
data_decoder_manager_, url_loader_factory_.get(), FrameTreeNodeId(),
kAuctionDevtoolsIds, kDefaultMainFrameOrigin, ip_address_space_,
network_partition_nonce_, GetScriptOrigin(), url,
BiddingAndAuctionServerKey{
std::string(reinterpret_cast<const char*>(kTestPublicKey),
sizeof(kTestPublicKey)),
kKeyIdStr},
compression_groups,
base::BindLambdaForTesting(
[&](TrustedSignalsFetcher::SignalsFetchResult result) {
out = std::move(result);
run_loop.Quit();
}));
// Check that the correct DataDecoder is constructed on fetch start, to
// prewarm the data decoder process.
EXPECT_EQ(data_decoder_manager_.GetHandleCountForTesting(
kDefaultMainFrameOrigin, GetScriptOrigin()),
1u);
run_loop.Run();
base::AutoLock auto_lock(lock_);
if (expect_url_not_requested_) {
EXPECT_FALSE(request_path_);
} else {
EXPECT_EQ(request_path_, url.PathForRequestPiece());
}
request_path_.reset();
return out;
}
TrustedSignalsFetcher::SignalsFetchResult
RequestScoringSignalsAndWaitForResult(
const std::map<int, std::vector<TrustedSignalsFetcher::ScoringPartition>>&
compression_groups,
std::optional<GURL> signals_url = std::nullopt) {
GURL url = signals_url.value_or(TrustedScoringSignalsUrl());
base::RunLoop run_loop;
TrustedSignalsFetcher::SignalsFetchResult out;
TrustedSignalsFetcher trusted_signals_fetcher;
trusted_signals_fetcher.FetchScoringSignals(
data_decoder_manager_, url_loader_factory_.get(), FrameTreeNodeId(),
kAuctionDevtoolsIds, kDefaultMainFrameOrigin, ip_address_space_,
network_partition_nonce_, GetScriptOrigin(), url,
BiddingAndAuctionServerKey{
std::string(reinterpret_cast<const char*>(kTestPublicKey),
sizeof(kTestPublicKey)),
kKeyIdStr},
compression_groups,
base::BindLambdaForTesting(
[&](TrustedSignalsFetcher::SignalsFetchResult result) {
out = std::move(result);
run_loop.Quit();
}));
// Check that the correct DataDecoder is constructed on fetch start, to
// prewarm the data decoder process.
EXPECT_EQ(data_decoder_manager_.GetHandleCountForTesting(
kDefaultMainFrameOrigin, GetScriptOrigin()),
1u);
run_loop.Run();
base::AutoLock auto_lock(lock_);
if (expect_url_not_requested_) {
EXPECT_FALSE(request_path_);
} else {
EXPECT_EQ(request_path_, url.PathForRequestPiece());
}
request_path_.reset();
return out;
}
std::string GetRequestBody() {
base::AutoLock auto_lock(lock_);
CHECK(request_body_.has_value());
std::string out = std::move(request_body_).value();
request_body_.reset();
return out;
}
size_t GetEncryptedRequestBodyLength() {
base::AutoLock auto_lock(lock_);
return encrypted_request_body_length_;
}
// Checks that the request body matches the provided string, which contains a
// hex-encoded representation of the expected result.
void ValidateRequestBodyHex(std::string_view expected_request_hex) {
std::string actual_response = GetRequestBody();
EXPECT_EQ(base::HexEncode(actual_response), expected_request_hex);
// If there's a mismatch, compare the non-hex-encoded string as well. This
// may give a better idea what's wrong when looking at test output.
if (HasNonfatalFailure()) {
std::string expected_response;
EXPECT_TRUE(
base::HexStringToString(expected_request_hex, &expected_response));
EXPECT_EQ(actual_response, expected_response);
}
}
// Checks that the request body matches the provided JSON. Converts the JSON
// input to a cbor string, adds a framing header and padding, and then check
// that matches the request body.
void ValidateRequestBodyJson(std::string_view expected_request_json) {
auto expected_request = auction_worklet::test::CreateKVv2RequestBody(
auction_worklet::test::ToCborString(expected_request_json));
ValidateRequestBodyHex(base::HexEncode(expected_request));
}
// Sets the response body string.
void SetResponseBody(std::string response_body, bool use_cleartext = false) {
base::AutoLock auto_lock(lock_);
response_body_ = std::move(response_body);
use_cleartext_response_body_ = use_cleartext;
}
// Convenience wrapper that calls CreateKVv2ResponseBody() on the provided
// values, and sets the resulting string as the response body.
void SetResponseBodyAndAddHeader(
std::string_view cbor_response_body,
std::optional<size_t> advertised_cbor_length = std::nullopt,
size_t padding_length = 0,
uint8_t compression_scheme = 0) {
SetResponseBody(auction_worklet::test::CreateKVv2ResponseBody(
cbor_response_body, advertised_cbor_length, padding_length,
compression_scheme));
}
// Helper to, in the case of a successfully fetched result, compare `result`
// to `expected_result`. Has an assertion failure if result indicates a
// failure.
void ValidateFetchResult(
const TrustedSignalsFetcher::SignalsFetchResult& result,
const TrustedSignalsFetcher::CompressionGroupResultMap& expected_result) {
ASSERT_TRUE(result.has_value());
ASSERT_EQ(result->size(), expected_result.size());
for (auto result_it = result->begin(),
expected_result_it = expected_result.begin();
result_it != result->end(); ++result_it, ++expected_result_it) {
// This is the compression group index of the expected result.
SCOPED_TRACE(expected_result_it->first);
EXPECT_EQ(result_it->first, expected_result_it->first);
EXPECT_EQ(result_it->second.compression_scheme,
expected_result_it->second.compression_scheme);
EXPECT_EQ(result_it->second.compression_group_data,
expected_result_it->second.compression_group_data);
EXPECT_EQ(result_it->second.ttl, expected_result_it->second.ttl);
}
}
// Checks that the fetch result matches what `DefaultResponseBody()` is
// expected to be parsed as.
void ValidateDefaultFetchResult(
const TrustedSignalsFetcher::SignalsFetchResult& result) {
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"compression group content", base::Milliseconds(100)));
ValidateFetchResult(result, expected_result);
}
// Sets response headers (other than Content-Type) for responses.
void SetResponseHeaders(
const std::vector<std::pair<std::string, std::string>>&
response_headers) {
base::AutoLock auto_lock(lock_);
response_headers_ = response_headers;
}
protected:
std::unique_ptr<net::test_server::HttpResponse> HandleSignalsRequest(
const net::test_server::HttpRequest& request) {
base::AutoLock auto_lock(lock_);
EXPECT_FALSE(request_path_);
// Don't record path for preflights - it should be recorded for the final
// request instead.
if (request.method_string != net::HttpRequestHeaders::kOptionsMethod) {
request_path_ = request.relative_url;
}
if (request.relative_url == kTrustedBiddingSignalsPath ||
request.relative_url == kTrustedScoringSignalsPath) {
EXPECT_EQ(
cors_preflight_expected_,
request.method_string == net::HttpRequestHeaders::kOptionsMethod);
EXPECT_FALSE(request_body_.has_value());
EXPECT_EQ(request.headers.find("Cookie"), request.headers.end());
EXPECT_THAT(request.headers,
testing::Contains(std::pair("Sec-Fetch-Mode", "cors")));
EXPECT_THAT(request.headers, testing::Contains(std::pair(
"Origin", script_origin_.Serialize())));
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
if (script_origin_is_same_origin_) {
EXPECT_THAT(request.headers, testing::Contains(std::pair(
"Sec-Fetch-Site", "same-origin")));
} else {
EXPECT_THAT(request.headers, testing::Contains(std::pair(
"Sec-Fetch-Site", "cross-site")));
// This needs to be sent both for the preflight and the actual request
// in the cross-origin case.
response->AddCustomHeader("Access-Control-Allow-Origin",
script_origin_.Serialize());
// If haven't see the options request yet, expect to see it before the
// actual request.
if (cors_preflight_expected_) {
if (request.method_string !=
net::HttpRequestHeaders::kOptionsMethod) {
ADD_FAILURE() << "Options method expected but got "
<< request.method_string;
return nullptr;
}
cors_preflight_expected_ = false;
EXPECT_THAT(request.headers,
testing::Contains(std::pair(
"Access-Control-Request-Headers", "content-type")));
response->AddCustomHeader("Access-Control-Allow-Headers",
"Content-Type");
EXPECT_FALSE(request.has_content);
response->set_code(net::HttpStatusCode::HTTP_NO_CONTENT);
return response;
}
}
EXPECT_THAT(
request.headers,
testing::Contains(std::pair(
"Content-Type", TrustedSignalsFetcher::kRequestMediaType)));
EXPECT_THAT(request.headers,
testing::Contains(std::pair(
"Accept", TrustedSignalsFetcher::kResponseMediaType)));
EXPECT_TRUE(request.has_content);
EXPECT_EQ(request.method_string, net::HttpRequestHeaders::kPostMethod);
auto config = quiche::ObliviousHttpHeaderKeyConfig::Create(
kKeyId, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256,
EVP_HPKE_AES_256_GCM);
EXPECT_TRUE(config.ok()) << config.status();
auto ohttp_gateway =
quiche::ObliviousHttpGateway::Create(
std::string(reinterpret_cast<const char*>(&kTestPrivateKey[0]),
sizeof(kTestPrivateKey)),
config.value())
.value();
encrypted_request_body_length_ = request.content.size();
auto plaintext_ohttp_request_body =
ohttp_gateway.DecryptObliviousHttpRequest(
request.content, TrustedSignalsFetcher::kRequestMediaType);
EXPECT_TRUE(plaintext_ohttp_request_body.ok())
<< plaintext_ohttp_request_body.status();
request_body_ = plaintext_ohttp_request_body->GetPlaintextData();
std::string response_body;
// Encryption doesn't support empty strings.
if (response_body_.size() > 0u && !use_cleartext_response_body_) {
auto context =
std::move(plaintext_ohttp_request_body).value().ReleaseContext();
auto ciphertext_ohttp_response_body =
ohttp_gateway.CreateObliviousHttpResponse(
response_body_, context,
TrustedSignalsFetcher::kResponseMediaType);
EXPECT_TRUE(ciphertext_ohttp_response_body.ok())
<< ciphertext_ohttp_response_body.status();
response_body =
ciphertext_ohttp_response_body->EncapsulateAndSerialize();
} else {
response_body = response_body_;
}
response->set_content_type(response_mime_type_);
response->set_code(response_status_code_);
response->set_content(response_body);
for (const auto& pair : response_headers_) {
response->AddCustomHeader(pair.first, pair.second);
}
return response;
}
return nullptr;
}
base::test::ScopedFeatureList feature_list_;
// Need to use an IO thread for the TestSharedURLLoaderFactory, which lives on
// the thread it's created on, to make network requests.
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::IO};
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
// Using different paths for bidding and scoring signals is not necessary, but
// does provide a little extra test coverage that the right URLs are requested
// from the server.
const std::string kTrustedBiddingSignalsPath = "/bidder-signals";
const std::string kTrustedScoringSignalsPath = "/scoring-signals";
const std::string kTrustedSignalsHost = "a.test";
// This value doesn't actually matter, as it's not tested by this file.
const base::flat_set<std::string> kAuctionDevtoolsIds{"auction_devtools_id"};
// Default values used by both both CreateBasicBiddingSignalsRequest() and
// CreateBasicScoringSignalsRequest(). They need to be fields of the test
// fixture to keep them alive, since the returned BiddingPartition holds onto
// non-owning raw pointers.
const url::Origin kDefaultMainFrameOrigin =
url::Origin::Create(GURL("https://host.test"));
const base::Value::Dict kDefaultAdditionalParams;
// Default values used by CreateBasicBiddingSignalsRequest().
const std::set<std::string> kDefaultInterestGroupNames{"group1"};
const std::set<std::string> kDefaultKeys{"key1"};
// Default values used by CreateBasicScoringSignalsRequest().
const GURL kDefaultRenderUrl{"https://render_url.test/foo"};
const std::set<GURL> kDefaultAdComponentRenderUrls;
DataDecoderManager data_decoder_manager_;
// Values returned for requests to the test server for
// `kTrustedBiddingSignalsPath`.
std::string response_mime_type_{TrustedSignalsFetcher::kResponseMediaType};
net::HttpStatusCode response_status_code_{net::HTTP_OK};
base::UnguessableToken network_partition_nonce_ =
base::UnguessableToken::Create();
base::Lock lock_;
// The origin of the interest group owner or seller, and whether it's
// same-origin to the signals URL. Populated when starting test server.
url::Origin script_origin_ GUARDED_BY(lock_);
bool script_origin_is_same_origin_ GUARDED_BY(lock_) = true;
// IP address space of the origin
network::mojom::IPAddressSpace ip_address_space_ =
network::mojom::IPAddressSpace::kLoopback;
// Whether an OPTIONS request is expected. When true, set to false once an
// options request is observed.
bool cors_preflight_expected_ GUARDED_BY(lock_) = false;
// If false, don't expect a request for signals to be handled.
bool expect_url_not_requested_ = false;
// Path of the last observed request. Don't record URL, because the embedded
// test server doesn't report the full requested URL.
std::optional<std::string> request_path_ GUARDED_BY(lock_);
// Size of the original encrypted request body.
size_t encrypted_request_body_length_ GUARDED_BY(lock_);
// The most recent request body received by the embedded test server,
// after decryption.
std::optional<std::string> request_body_ GUARDED_BY(lock_);
// The response body to reply with.
std::string response_body_ GUARDED_BY(lock_);
// If true, the response body is not encrypted, which should result in an
// error.
bool use_cleartext_response_body_ GUARDED_BY(lock_) = false;
// Header values to include in the response. Default value is needed to allow
// response to be used at all.
std::vector<std::pair<std::string, std::string>> response_headers_
GUARDED_BY(lock_){{"Ad-Auction-Allowed", "true"}};
net::test_server::EmbeddedTestServer embedded_test_server_{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
// URLLoaderFactory that makes real network requests.
scoped_refptr<network::TestSharedURLLoaderFactory> url_loader_factory_{
base::MakeRefCounted<network::TestSharedURLLoaderFactory>(
/*network_service=*/nullptr,
/*is_trusted=*/true)};
};
TEST_F(TrustedSignalsFetcherTest, BiddingSignals404) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
response_status_code_ = net::HTTP_NOT_FOUND;
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf("Failed to load %s HTTP status = 404 Not Found.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
// Test various permutations of the "Ad-Auction-Allowed" and "X-Allow-FLEDGE"
// header being present and absent.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsAdAuctionAllowed) {
const struct {
std::vector<std::pair<std::string, std::string>> headers;
bool expect_success;
} kTestCases[] = {
{{{"Ad-Auction-Allowed", "true"}}, true},
{{{"X-Allow-FLEDGE", "true"}}, true},
{{}, false},
{{{"Ad-Auction-Allowed", "false"}}, false},
{{{"X-Allow-FLEDGE", "false"}}, false},
};
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
for (const auto& test_case : kTestCases) {
SetResponseHeaders(test_case.headers);
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
EXPECT_EQ(result.has_value(), test_case.expect_success);
if (!result.has_value()) {
EXPECT_EQ(result.error(),
base::StringPrintf(
"Rejecting load of %s due to lack of Ad-Auction-Allowed: "
"true (or the deprecated X-Allow-FLEDGE: true).",
TrustedBiddingSignalsUrl().spec().c_str()));
}
}
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsRedirect) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
GURL server_redirect_url = embedded_test_server_.GetURL(
kTrustedSignalsHost,
"/server-redirect?" + TrustedBiddingSignalsUrl().spec());
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request,
server_redirect_url);
ASSERT_FALSE(result.has_value());
// RedirectMode::kError results in ERR_FAILED errors on redirects, which
// results in rather unhelpful error messages.
EXPECT_EQ(result.error(),
base::StringPrintf("Unexpected redirect on %s.",
server_redirect_url.spec().c_str()));
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsMimeType) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
// Use the request media type instead of the response one.
response_mime_type_ = TrustedSignalsFetcher::kRequestMediaType;
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf("Rejecting load of %s due to unexpected MIME type.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsCanSetNoCookies) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
// Request trusted bidding signals using a URL that tries to set a cookie.
GURL set_cookie_url = embedded_test_server_.GetURL(
kTrustedSignalsHost, "/set-cookie?a=1;Secure;SameSite=None");
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request,
set_cookie_url);
// Specific failure reason doesn't really matter for this test, or even that
// it failed. What does matter is the fetch response was successfully
// received, so best to test the request completed in the expected manner.
EXPECT_EQ(result.error(),
base::StringPrintf(
"Rejecting load of %s due to lack of Ad-Auction-Allowed: true "
"(or the deprecated X-Allow-FLEDGE: true).",
set_cookie_url.spec().c_str()));
// Make sure no cookie was set.
base::RunLoop run_loop;
mojo::Remote<network::mojom::CookieManager> cookie_manager;
url_loader_factory_->network_context()->GetCookieManager(
cookie_manager.BindNewPipeAndPassReceiver());
cookie_manager->GetAllCookies(
base::BindLambdaForTesting([&](const net::CookieList& cookies) {
EXPECT_TRUE(cookies.empty());
run_loop.Quit();
}));
run_loop.Run();
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsHasNoCookies) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
// Set a same-site none cookie on the trusted signals server's origin.
mojo::Remote<network::mojom::CookieManager> cookie_manager;
url_loader_factory_->network_context()->GetCookieManager(
cookie_manager.BindNewPipeAndPassReceiver());
net::CookieInclusionStatus status;
std::unique_ptr<net::CanonicalCookie> cookie = net::CanonicalCookie::Create(
TrustedBiddingSignalsUrl(), "a=1; Secure; SameSite=None",
base::Time::Now(),
/*server_time=*/std::nullopt,
/*cookie_partition_key=*/std::nullopt, net::CookieSourceType::kHTTP,
&status);
ASSERT_TRUE(cookie);
base::RunLoop run_loop;
cookie_manager->SetCanonicalCookie(
*cookie, TrustedBiddingSignalsUrl(),
net::CookieOptions::MakeAllInclusive(),
base::BindLambdaForTesting([&](net::CookieAccessResult result) {
EXPECT_TRUE(result.status.IsInclude());
run_loop.Quit();
}));
run_loop.Run();
// Request trusted bidding signals. The request handler will cause the test to
// fail if it sees a cookie header.
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsNoKeys) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
const std::set<std::string> kNoKeys;
bidding_signals_request[0][0].keys = kNoKeys;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1" ]
},
{
"tags": [ "keys" ],
"data": []
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsOneKey) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsMultipleKeys) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
const std::set<std::string> kKeys = {"key1", "key2", "key3"};
bidding_signals_request[0][0].keys = kKeys;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1" ]
},
{
"tags": [ "keys" ],
"data": [ "key1", "key2", "key3" ]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsMultipleInterestGroups) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
const std::set<std::string> kInterestGroupNames = {"group1", "group2",
"group3"};
bidding_signals_request[0][0].interest_group_names = kInterestGroupNames;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1", "group2", "group3" ]
},
{
"tags": [ "keys" ],
"data": [ "key1" ]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsOneAdditionalParam) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
base::Value::Dict additional_params;
additional_params.Set("foo", base::Value("bar"));
bidding_signals_request[0][0].additional_params = additional_params;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"metadata": { "foo": "bar" },
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1" ]
},
{
"tags": [ "keys" ],
"data": [ "key1" ]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsMultipleAdditionalParams) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
base::Value::Dict additional_params;
additional_params.Set("foo", "bar");
additional_params.Set("Foo", "bAr");
additional_params.Set("oof", "rab");
bidding_signals_request[0][0].additional_params = additional_params;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"metadata": {
"foo": "bar",
"Foo": "bAr",
"oof": "rab",
},
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1" ]
},
{
"tags": [ "keys" ],
"data": [ "key1" ]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Test the simplest request case, with no optional parameters.
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsMinimalRequest) {
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
ValidateDefaultFetchResult(
RequestScoringSignalsAndWaitForResult(scoring_signals_request));
ValidateRequestBodyHex(kBasicScoringSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsOneAdComponentRenderUrl) {
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
const std::set<GURL> kComponentRenderUrls{GURL("https://component.test/bar")};
scoring_signals_request[0][0].component_render_urls = kComponentRenderUrls;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url.test/foo" ]
},
{
"tags": [ "adComponentRenderURLs" ],
"data": [ "https://component.test/bar" ]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestScoringSignalsAndWaitForResult(scoring_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsMultipleAdComponentRenderUrls) {
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
const std::set<GURL> kComponentRenderUrls{
GURL("https://component1.test/"),
GURL("https://component1.test/bar"),
GURL("https://component1.test/foo"),
GURL("https://component2.test/baz"),
kDefaultRenderUrl,
};
scoring_signals_request[0][0].component_render_urls = kComponentRenderUrls;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url.test/foo" ]
},
{
"tags": [ "adComponentRenderURLs" ],
"data": [
"https://component1.test/",
"https://component1.test/bar",
"https://component1.test/foo",
"https://component2.test/baz",
"https://render_url.test/foo"
]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestScoringSignalsAndWaitForResult(scoring_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsOneAdditionalParam) {
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
base::Value::Dict additional_params;
additional_params.Set("foo", base::Value("bar"));
scoring_signals_request[0][0].additional_params = additional_params;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"metadata": { "foo": "bar" },
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url.test/foo" ]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestScoringSignalsAndWaitForResult(scoring_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsMultipleAdditionalParams) {
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
base::Value::Dict additional_params;
additional_params.Set("foo", "bar");
additional_params.Set("Foo", "bAr");
additional_params.Set("oof", "rab");
scoring_signals_request[0][0].additional_params = additional_params;
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"metadata": {
"foo": "bar",
"Foo": "bAr",
"oof": "rab",
},
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url.test/foo" ]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestScoringSignalsAndWaitForResult(scoring_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Test a single compression group with a single partition, where neither has
// the index 0.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsNoZeroIndices) {
std::vector<TrustedSignalsFetcher::BiddingPartition> bidding_partitions;
bidding_partitions.emplace_back(/*partition_id=*/7,
&kDefaultInterestGroupNames, &kDefaultKeys,
&kDefaultAdditionalParams,
/*buyer_tkv_signals=*/nullptr);
std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>
bidding_signals_request;
bidding_signals_request.emplace(3, std::move(bidding_partitions));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 3,
"id": 7,
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1" ]
},
{
"tags": [ "keys" ],
"data": [ "key1" ]
}
]
}
]
})";
// The response similarly only includes information for compression group 3.
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 3,
"content": "content"
}
]
})"));
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
3, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content", base::Milliseconds(0)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Test that the expected amount of padding is added to requests.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsRequestPadding) {
const struct {
size_t interest_group_name_length;
// Test the encrypted and unecrypted request body. The encrypted body
// length, which should always be a power 2, is what's actually publicly
// visible. The others are useful for debugging.
size_t expected_encrypted_body_length;
size_t expected_body_length;
size_t expected_padding;
} kTestCases[] = {
{31, 256, 201, 1},
{32, 256, 201, 0},
{33, 512, 457, 255},
// 286 is less than 31+256 because strings in cbor have variable-length
// length prefixes.
{286, 512, 457, 1},
{287, 512, 457, 0},
{288, 1024, 969, 511},
};
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.interest_group_name_length);
std::string name = std::string(test_case.interest_group_name_length, 'a');
std::set<std::string> interest_group_names = {name};
bidding_signals_request[0][0].interest_group_names = interest_group_names;
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
EXPECT_EQ(GetEncryptedRequestBodyLength(),
test_case.expected_encrypted_body_length);
std::string request_body = GetRequestBody();
size_t padding =
request_body.size() - request_body.find_last_not_of('\0') - 1;
EXPECT_EQ(request_body.size(), test_case.expected_body_length);
EXPECT_EQ(padding, test_case.expected_padding);
// Also test the entire request body directly. The above checks provide some
// protection against issues in CreateKVv2RequestBody(), which is largely
// copied from TrustedSignalsFetcher.
EXPECT_EQ(request_body, auction_worklet::test::CreateKVv2RequestBody(
auction_worklet::test::ToCborString(JsReplace(
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ $1 ]
},
{
"tags": [ "keys" ],
"data": [ "key1" ]
}
]
}
]
})",
name))));
}
}
// Test that the expected amount of padding is added to requests.
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsRequestPadding) {
const struct {
size_t render_url_path_length;
// Test the encrypted and unecrypted request body. The encrypted body
// length, which should always be a power 2, is what's actually publicly
// visible. The others are useful for debugging.
size_t expected_encrypted_body_length;
size_t expected_body_length;
size_t expected_padding;
} kTestCases[] = {
{45, 256, 201, 1},
{46, 256, 201, 0},
{47, 512, 457, 255},
// 300 is less than 45+256 because strings in cbor have variable-length
// length prefixes.
{300, 512, 457, 1},
{301, 512, 457, 0},
{302, 1024, 969, 511},
};
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.render_url_path_length);
GURL render_url = GURL("https://foo.test/" +
std::string(test_case.render_url_path_length, 'a'));
scoring_signals_request[0][0].render_url = render_url;
ValidateDefaultFetchResult(
RequestScoringSignalsAndWaitForResult(scoring_signals_request));
EXPECT_EQ(GetEncryptedRequestBodyLength(),
test_case.expected_encrypted_body_length);
std::string request_body = GetRequestBody();
size_t padding =
request_body.size() - request_body.find_last_not_of('\0') - 1;
EXPECT_EQ(request_body.size(), test_case.expected_body_length);
EXPECT_EQ(padding, test_case.expected_padding);
// Also test the entire request body directly. The above checks provide some
// protection against issues in CreateKVv2RequestBody(), which is largely
// copied from TrustedSignalsFetcher.
EXPECT_EQ(request_body, auction_worklet::test::CreateKVv2RequestBody(
auction_worklet::test::ToCborString(JsReplace(
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ $1 ]
}
]
}
]
})",
render_url))));
}
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsResponseBodyShorterThanHeader) {
for (int length = 0; length < 5; ++length) {
SetResponseBody(std::string(length, 0));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf(
"Failed to load %s: Response body is shorter than a "
"message/ad-auction-trusted-signals-response header.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsResponseBodyUnencrypted) {
SetResponseBody(DefaultResponseBody(), /*use_cleartext=*/true);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf("Failed to load %s: OHTTP decryption failed.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
// Receiving CBOR without a header in the response body should result in
// failure.
TEST_F(TrustedSignalsFetcherTest, NoResponseBodyHeader) {
SetResponseBody(DefaultResponseBody());
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
// Don't check the specific error - it depends on the specific details of the
// CBOR representation of DefaultResponseBody().
ASSERT_FALSE(result.has_value());
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
// This test does not actually gzip the body. The fetcher doesn't try to
// decompress anything, so that should be fine.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsCompressionSchemeGzip) {
SetResponseBodyAndAddHeader(DefaultResponseBody(),
/*advertised_cbor_length=*/std::nullopt,
/*padding_length=*/0,
/*compression_scheme=*/2);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kGzip,
"compression group content", base::Milliseconds(100)));
ValidateFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request),
expected_result);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsCompressionSchemeUnsupported) {
for (int compression_scheme : {1, 3}) {
SetResponseBodyAndAddHeader(DefaultResponseBody(),
/*advertised_cbor_length=*/std::nullopt,
/*padding_length=*/0, compression_scheme);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf(
"Failed to load %s: Unsupported compression scheme: %u.",
TrustedBiddingSignalsUrl().spec().c_str(), compression_scheme));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
TEST_F(TrustedSignalsFetcherTest,
BiddingSignalsCompressionSchemeHighOrderBitsIgnored) {
// Everything but the low order two bits of the compression scheme should be
// ignored, so this should be treated as scheme 2 - gzip.
SetResponseBodyAndAddHeader(DefaultResponseBody(),
/*advertised_cbor_length=*/std::nullopt,
/*padding_length=*/0,
/*compression_scheme=*/0xFE);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kGzip,
"compression group content", base::Milliseconds(100)));
ValidateFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request),
expected_result);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
// If the advertised length is longer than the response, the request should
// fail, even if it's otherwise a valid CBOR response. This test also checks the
// case where the maximum possible length is received, to make sure there are no
// overflow/underflow issues.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsAdvertisedLengthTooLong) {
const std::string response_body = DefaultResponseBody();
const size_t kTestCases[] = {response_body.length() + 1,
std::numeric_limits<uint32_t>::max()};
for (size_t advertised_cbor_length : kTestCases) {
SetResponseBodyAndAddHeader(response_body, advertised_cbor_length);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf(
"Failed to load %s: Length header exceeds body size.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
// If the advertised shorter is longer than the response, the remaining bytes
// should be ignored, even if they make an otherwise valid CBOR response.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsAdvertisedLengthTooShort) {
SetResponseBodyAndAddHeader(DefaultResponseBody(),
DefaultResponseBody().length() / 2 - 1);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf("Failed to load %s: Failed to parse response as CBOR.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsResponsePadding) {
// No need to check length 0 - that's the default amount of padding added by
// SetResponseBodyAndAddHeader().
for (size_t padding_length : {1, 2, 16, 1023, 1024}) {
SetResponseBodyAndAddHeader(DefaultResponseBody(),
/*advertised_cbor_length=*/std::nullopt,
padding_length);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
// Test the case where there are valid framing headers, but the response body is
// not CBOR.
TEST_F(TrustedSignalsFetcherTest, NotCbor) {
const std::string_view kTestCases[] = {
// This is "This is not CBOR." as a hex string.
"\x54\x68\x69\x73\x20\x69\x73\x20\x6E\x6F\x74\x20\x43\x42\x4F\x52\x2E",
// Null in CBOR, which is currently rejected as not being CBOR by the CBOR
// parser, which is a little weird. Seems
// best to test this case, though, and if we ever do parse it, move it
// into the next test.
"\xF6",
// CBOR has a lot of values that don't map to JSON or even to Javascript
// objects. This is a very incomplete set of some types of them, without
// delving into the spec.
// Undefined in CBOR.
"\xF7",
// An unassigned CBOR value.
"\xF0",
// A reserved CBOR value.
"\xF8\x20",
};
for (const std::string_view& test_string : kTestCases) {
SCOPED_TRACE(base::HexEncode(test_string));
SetResponseBodyAndAddHeader(test_string);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf(
"Failed to load %s: Failed to parse response as CBOR.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
// Test cases where there's a valid framing header, and the response is CBOR,
// but it's not a map.
TEST_F(TrustedSignalsFetcherTest, NotCborMap) {
const std::string_view kTestCases[] = {
R"(true)",
R"("This is a string")",
R"(42)",
R"(["array"])",
};
for (const std::string_view& test_string : kTestCases) {
SCOPED_TRACE(test_string);
SetResponseBodyAndAddHeader(
auction_worklet::test::ToKVv2ResponseCborString(test_string));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf("Failed to load %s: Response body is not a map.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
TEST_F(TrustedSignalsFetcherTest, NoCompressionGroupMap) {
// An empty map.
SetResponseBodyAndAddHeader(
auction_worklet::test::ToKVv2ResponseCborString("{}"));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf(
"Failed to load %s: Response is missing compressionGroups array.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, CompressionGroupsNotArray) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({"compressionGroups": {}})"));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf(
"Failed to load %s: Response is missing compressionGroups array.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, NoCompressionGroups) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({"compressionGroups": []})"));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
// The fetch succeeds, and the result is an empty map. The cache layer, which
// maps requests to fetched compression groups, will consider this a failure,
// but the fetcher considers this a valid result.
ValidateFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request),
TrustedSignalsFetcher::CompressionGroupResultMap());
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, CompressionGroupNotMap) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({"compressionGroups": [[]]})"));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf(
"Failed to load %s: Compression group is not of type map.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest,
CompressionGroupWithBadOrNoCompressionGroupId) {
const std::string_view kTestCases[] = {
R"({
"compressionGroups": [
{
"content" : "content"
}
]
})",
R"({
"compressionGroups": [
{
"compressionGroupId": "Jim",
"content" : "content"
}
]
})",
R"({
"compressionGroups": [
{
"compressionGroupId": -1,
"content" : "content"
}
]
})",
R"({
"compressionGroups": [
{
"compressionGroupId": 0.0,
"content" : "content"
}
]
})",
};
for (const std::string_view test_string : kTestCases) {
SCOPED_TRACE(test_string);
SetResponseBodyAndAddHeader(
auction_worklet::test::ToKVv2ResponseCborString(test_string));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf("Failed to load %s: Compression group must have a "
"non-negative integer compressionGroupId.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
TEST_F(TrustedSignalsFetcherTest, CompressionGroupWithBadOrNoContent) {
// Each test case uses a different compression group ID to test the error
// output. Note that TrustedSignalsFetcher has no requirement that returned
// compression groups match requested compression groups. That's enforced by
// the cache layer, since it has to match compression groups to requests it
// sent out, anyways.
const std::vector<std::string_view> kTestCases = {
R"({
"compressionGroups": [
{
"compressionGroupId": 0
}
]
})",
R"({
"compressionGroups": [
{
"compressionGroupId": 1,
"content" : 5
}
]
})",
R"({
"compressionGroups": [
{
"compressionGroupId": 2,
"content" : ["content"]
}
]
})",
// This content type is a string instead of a binary string, which should
// result in an error.
R"({
"compressionGroups": [
{
"compressionGroupId": 3,
"content" : "content"
}
]
})",
};
for (size_t i = 0; i < kTestCases.size(); ++i) {
SCOPED_TRACE(kTestCases[i]);
// Note that this uses ToCborString() to convert the JSON to a CBOR string
// rather than ToKVv2ResponseCborString(). This results in the "content"
// fields not being encoded as binary strings, but rather as whatever CBOR
// type corresponds to the JSON type of the "content" field.
SetResponseBodyAndAddHeader(
auction_worklet::test::ToCborString(kTestCases[i]));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf("Failed to load %s: Compression group %" PRIuS
" missing binary string \"content\".",
TrustedBiddingSignalsUrl().spec().c_str(), i));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
TEST_F(TrustedSignalsFetcherTest, CompressionGroupWithBadTtl) {
// Each test case uses a different compression group ID to test the error
// output. Note that TrustedSignalsFetcher has no requirement that returned
// compression groups match requested compression groups. That's enforced by
// the cache layer, since it has to match compression groups to requests it
// sent out, anyways.
const std::vector<std::string_view> kTestCases = {
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content",
"ttlMs": "grapefruit"
}
]
})",
R"({
"compressionGroups": [
{
"compressionGroupId": 1,
"content": "content",
"ttlMs": 0.5
}
]
})",
};
for (size_t i = 0; i < kTestCases.size(); ++i) {
SCOPED_TRACE(kTestCases[i]);
SetResponseBodyAndAddHeader(
auction_worklet::test::ToKVv2ResponseCborString(kTestCases[i]));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf("Failed to load %s: Compression group %" PRIuS
" ttlMs value is not an integer.",
TrustedBiddingSignalsUrl().spec().c_str(), i));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
// `ttlMs` is an optional field. When not present, we currently default to a
// value of 0.
TEST_F(TrustedSignalsFetcherTest, CompressionGroupWithNoTtl) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content"
}
]
})"));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content", base::Milliseconds(0)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, CompressionGroupWithZeroTtl) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content",
"ttlMs": 0
}
]
})"));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content", base::Milliseconds(0)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
// Negative TTLs are allows, and are treated as if they were zero.
TEST_F(TrustedSignalsFetcherTest, CompressionGroupWithNegativeTtl) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content",
"ttlMs": -1
}
]
})"));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content", base::Milliseconds(0)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsMultiplePartitions) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto* bidding_partitions = &bidding_signals_request[0];
const std::set<std::string> kInterestGroupNames2{"group2"};
const std::set<std::string> kKeys2{"key2"};
base::Value::Dict additional_params2;
additional_params2.Set("foo", "bar");
bidding_partitions->emplace_back(
/*partition_id=*/1, &kInterestGroupNames2, &kKeys2, &additional_params2,
/*buyer_tkv_signals=*/nullptr);
const std::set<std::string> kInterestGroupNames3{"group1", "group2",
"group3"};
const std::set<std::string> kKeys3{"key1", "key2", "key3"};
base::Value::Dict additional_params3;
additional_params3.Set("foo2", "bar2");
bidding_partitions->emplace_back(/*partition_id=*/2, &kInterestGroupNames3,
&kKeys3, &additional_params3,
/*buyer_tkv_signals=*/nullptr);
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1" ]
},
{
"tags": [ "keys" ],
"data": [ "key1" ]
}
]
},
{
"compressionGroupId": 0,
"id": 1,
"metadata": { "foo": "bar" },
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group2" ]
},
{
"tags": [ "keys" ],
"data": [ "key2" ]
}
]
},
{
"compressionGroupId": 0,
"id": 2,
"metadata": { "foo2": "bar2" },
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1", "group2", "group3" ]
},
{
"tags": [ "keys" ],
"data": [ "key1", "key2", "key3" ]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestBiddingSignalsAndWaitForResult(bidding_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsMultiplePartitions) {
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
auto* scoring_partitions = &scoring_signals_request[0];
const GURL renderUrl2("https://render_url2.test/");
const std::set<GURL> kAdComponentRenderUrls2{
GURL("https://component2.test/")};
base::Value::Dict additional_params2;
additional_params2.Set("foo", "bar");
scoring_partitions->emplace_back(
/*partition_id=*/1, &renderUrl2, &kAdComponentRenderUrls2,
&additional_params2, /*seller_tkv_signals=*/nullptr);
const GURL renderUrl3("https://render_url3.test/");
const std::set<GURL> kAdComponentRenderUrls3{
GURL("https://component3.test/bar"), GURL("https://component3.test/foo")};
base::Value::Dict additional_params3;
additional_params3.Set("foo2", "bar2");
scoring_partitions->emplace_back(
/*partition_id=*/2, &renderUrl3, &kAdComponentRenderUrls3,
&additional_params3, /*seller_tkv_signals=*/nullptr);
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url.test/foo" ]
}
]
},
{
"compressionGroupId": 0,
"id": 1,
"metadata": { "foo": "bar" },
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url2.test/" ]
},
{
"tags": [ "adComponentRenderURLs" ],
"data": [ "https://component2.test/" ]
}
]
},
{
"compressionGroupId": 0,
"id": 2,
"metadata": { "foo2": "bar2" },
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url3.test/" ]
},
{
"tags": [ "adComponentRenderURLs" ],
"data": [
"https://component3.test/bar",
"https://component3.test/foo"
]
}
]
}
]
})";
ValidateDefaultFetchResult(
RequestScoringSignalsAndWaitForResult(scoring_signals_request));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Test that a fetch fails when there are two compression groups with the same
// ID in the response.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsDuplicateCompressionGroups) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content"
},
{
"compressionGroupId": 0,
"content": "content"
}
]
})"));
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf("Failed to load %s: Response contains two "
"compression groups with id 0.",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsMultipleCompressionGroups) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
const std::set<std::string> kInterestGroupNames2{"group2"};
const std::set<std::string> kKeys2{"key2"};
base::Value::Dict additional_params2;
additional_params2.Set("foo", "bar");
std::vector<TrustedSignalsFetcher::BiddingPartition> bidding_partitions2;
bidding_partitions2.emplace_back(/*partition_id=*/0, &kInterestGroupNames2,
&kKeys2, &additional_params2,
/*buyer_tkv_signals=*/nullptr);
bidding_signals_request.emplace(1, std::move(bidding_partitions2));
const std::set<std::string> kInterestGroupNames3{"group1", "group2",
"group3"};
const std::set<std::string> kKeys3{"key1", "key2", "key3"};
const std::string kHostname3{"host3.test"};
base::Value::Dict additional_params3;
additional_params3.Set("foo2", "bar2");
std::vector<TrustedSignalsFetcher::BiddingPartition> bidding_partitions3;
bidding_partitions3.emplace_back(/*partition_id=*/0, &kInterestGroupNames3,
&kKeys3, &additional_params3,
/*buyer_tkv_signals=*/nullptr);
bidding_signals_request.emplace(2, std::move(bidding_partitions3));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1" ]
},
{
"tags": [ "keys" ],
"data": [ "key1" ]
}
]
},
{
"compressionGroupId": 1,
"id": 0,
"metadata": { "foo": "bar" },
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group2" ]
},
{
"tags": [ "keys" ],
"data": [ "key2" ]
}
]
},
{
"compressionGroupId": 2,
"id": 0,
"metadata": { "foo2": "bar2" },
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1", "group2", "group3" ]
},
{
"tags": [ "keys" ],
"data": [ "key1", "key2", "key3" ]
}
]
}
]
})";
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content1",
"ttlMs": 10
},
{
"compressionGroupId": 1,
"content": "content2"
},
{
"compressionGroupId": 2,
"content": "content3",
"ttlMs": 150
}
]
})"));
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content1", base::Milliseconds(10)));
expected_result.try_emplace(
1, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content2", base::Milliseconds(0)));
expected_result.try_emplace(
2, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content3", base::Milliseconds(150)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsMultipleCompressionGroups) {
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
const GURL renderUrl2("https://render_url2.test/");
const std::set<GURL> kAdComponentRenderUrls2{
GURL("https://component2.test/")};
base::Value::Dict additional_params2;
additional_params2.Set("foo", "bar");
std::vector<TrustedSignalsFetcher::ScoringPartition> scoring_partitions2;
scoring_partitions2.emplace_back(
/*partition_id=*/0, &renderUrl2, &kAdComponentRenderUrls2,
&additional_params2, /*seller_tkv_signals=*/nullptr);
scoring_signals_request.emplace(1, std::move(scoring_partitions2));
const GURL renderUrl3("https://render_url3.test/");
const std::set<GURL> kAdComponentRenderUrls3{
GURL("https://component3.test/bar"), GURL("https://component3.test/foo")};
base::Value::Dict additional_params3;
additional_params3.Set("foo2", "bar2");
std::vector<TrustedSignalsFetcher::ScoringPartition> scoring_partitions3;
scoring_partitions3.emplace_back(
/*partition_id=*/0, &renderUrl3, &kAdComponentRenderUrls3,
&additional_params3, /*seller_tkv_signals=*/nullptr);
scoring_signals_request.emplace(2, std::move(scoring_partitions3));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url.test/foo" ]
}
]
},
{
"compressionGroupId": 1,
"id": 0,
"metadata": { "foo": "bar" },
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url2.test/" ]
},
{
"tags": [ "adComponentRenderURLs" ],
"data": [ "https://component2.test/" ]
}
]
},
{
"compressionGroupId": 2,
"id": 0,
"metadata": { "foo2": "bar2" },
"arguments": [
{
"tags": [ "renderURLs" ],
"data": [ "https://render_url3.test/" ]
},
{
"tags": [ "adComponentRenderURLs" ],
"data": [
"https://component3.test/bar",
"https://component3.test/foo"
]
}
]
}
]
})";
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content1",
"ttlMs": 10
},
{
"compressionGroupId": 1,
"content": "content2"
},
{
"compressionGroupId": 2,
"content": "content3",
"ttlMs": 150
}
]
})"));
auto result = RequestScoringSignalsAndWaitForResult(scoring_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content1", base::Milliseconds(10)));
expected_result.try_emplace(
1, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content2", base::Milliseconds(0)));
expected_result.try_emplace(
2, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content3", base::Milliseconds(150)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Test that the entire fetch fails when one of the requested partitions has an
// error.
TEST_F(TrustedSignalsFetcherTest,
BiddingSignalsMultipleCompressionGroupsFailsWhenOneBad) {
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
const std::set<std::string> kInterestGroupNames2{"group2"};
const std::set<std::string> kKeys2{"key2"};
base::Value::Dict additional_params2;
additional_params2.Set("foo", "bar");
std::vector<TrustedSignalsFetcher::BiddingPartition> bidding_partitions2;
bidding_partitions2.emplace_back(/*partition_id=*/0, &kInterestGroupNames2,
&kKeys2, &additional_params2,
/*buyer_tkv_signals=*/nullptr);
bidding_signals_request.emplace(1, std::move(bidding_partitions2));
const std::set<std::string> kInterestGroupNames3{"group1", "group2",
"group3"};
const std::set<std::string> kKeys3{"key1", "key2", "key3"};
base::Value::Dict additional_params3;
additional_params3.Set("foo2", "bar2");
std::vector<TrustedSignalsFetcher::BiddingPartition> bidding_partitions3;
bidding_partitions3.emplace_back(/*partition_id=*/0, &kInterestGroupNames3,
&kKeys3, &additional_params3,
/*buyer_tkv_signals=*/nullptr);
bidding_signals_request.emplace(2, std::move(bidding_partitions3));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"partitions": [
{
"compressionGroupId": 0,
"id": 0,
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1" ]
},
{
"tags": [ "keys" ],
"data": [ "key1" ]
}
]
},
{
"compressionGroupId": 1,
"id": 0,
"metadata": { "foo": "bar" },
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group2" ]
},
{
"tags": [ "keys" ],
"data": [ "key2" ]
}
]
},
{
"compressionGroupId": 2,
"id": 0,
"metadata": { "foo2": "bar2" },
"arguments": [
{
"tags": [ "interestGroupNames" ],
"data": [ "group1", "group2", "group3" ]
},
{
"tags": [ "keys" ],
"data": [ "key1", "key2", "key3" ]
}
]
}
]
})";
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content1",
"ttlMs": 10
},
{
"compressionGroupId": 1
},
{
"compressionGroupId": 2,
"content": "content3",
"ttlMs": 150
}
]
})"));
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf("Failed to load %s: Compression group 1 missing "
"binary string \"content\".",
TrustedBiddingSignalsUrl().spec().c_str()));
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsCrossOrigin) {
// Test cross-origin requests both in the case
// `kProtectedAudienceCorsSafelistKVv2Signals` is disabled and when it's
// enabled. In only the first case should there be a CORS preflight.
for (bool add_content_type_to_cors_safelist : {false, true}) {
SCOPED_TRACE(add_content_type_to_cors_safelist);
base::test::ScopedFeatureList feature_list;
if (add_content_type_to_cors_safelist) {
feature_list.InitAndEnableFeature(
network::features::kProtectedAudienceCorsSafelistKVv2Signals);
} else {
feature_list.InitAndDisableFeature(
network::features::kProtectedAudienceCorsSafelistKVv2Signals);
}
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content"
}
]
})"));
SetCrossOrigin(
/*cors_preflight_expected=*/!add_content_type_to_cors_safelist);
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result =
RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content", base::Milliseconds(0)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsCrossOriginLNAFailure) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content"
}
]
})"));
SetCrossOrigin();
// Set IP Address space of the origin to be public, making signal requests LNA
// requests (as embedded_test_server_ is in IPAddressSpace::kLoopback)
ip_address_space_ = network::mojom::IPAddressSpace::kPublic;
// Don't expect signals requests to get handled.
expect_url_not_requested_ = true;
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf("Failed to load %s error = "
"net::ERR_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_CHECKS.",
TrustedBiddingSignalsUrl().spec().c_str()));
}
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsCrossOriginNotLNASuccess) {
// Treat all requests for signals as coming to a server in
// IPAddressSpace::kPublic, so it shouldn't be considered an LNA request.
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
network::switches::kIpAddressSpaceOverrides,
base::StringPrintf(
"%s=public",
embedded_test_server_.host_port_pair().ToString().c_str()));
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content"
}
]
})"));
SetCrossOrigin();
ip_address_space_ = network::mojom::IPAddressSpace::kPublic;
auto bidding_signals_request = CreateBasicBiddingSignalsRequest();
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content", base::Milliseconds(0)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyHex(kBasicBiddingSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsCrossOrigin) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content"
}
]
})"));
SetCrossOrigin();
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
auto result = RequestScoringSignalsAndWaitForResult(scoring_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content", base::Milliseconds(0)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyHex(kBasicScoringSignalsRequestBody);
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsCrossOriginLNAFailure) {
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content"
}
]
})"));
SetCrossOrigin();
// Set IP Address space of the origin to be public, making signal requests LNA
// requests (as embedded_test_server_ is in IPAddressSpace::kLoopback)
ip_address_space_ = network::mojom::IPAddressSpace::kPublic;
// Don't expect signals requests to get handled.
expect_url_not_requested_ = true;
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
auto result = RequestScoringSignalsAndWaitForResult(scoring_signals_request);
ASSERT_FALSE(result.has_value());
EXPECT_EQ(
result.error(),
base::StringPrintf("Failed to load %s error = "
"net::ERR_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_CHECKS.",
TrustedScoringSignalsUrl().spec().c_str()));
}
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsCrossOriginNotLNASuccess) {
// Treat all requests for signals as coming to a server in
// IPAddressSpace::kPublic, so it shouldn't be considered an LNA request.
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
network::switches::kIpAddressSpaceOverrides,
base::StringPrintf(
"%s=public",
embedded_test_server_.host_port_pair().ToString().c_str()));
SetResponseBodyAndAddHeader(auction_worklet::test::ToKVv2ResponseCborString(
R"({
"compressionGroups": [
{
"compressionGroupId": 0,
"content": "content"
}
]
})"));
SetCrossOrigin();
ip_address_space_ = network::mojom::IPAddressSpace::kPublic;
auto scoring_signals_request = CreateBasicScoringSignalsRequest();
auto result = RequestScoringSignalsAndWaitForResult(scoring_signals_request);
TrustedSignalsFetcher::CompressionGroupResultMap expected_result;
expected_result.try_emplace(
0, CreateCompressionGroupResult(
auction_worklet::mojom::TrustedSignalsCompressionScheme::kNone,
"content", base::Milliseconds(0)));
ValidateFetchResult(result, expected_result);
ValidateRequestBodyHex(kBasicScoringSignalsRequestBody);
}
// Tests that the correct IsolationInfo is used.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsIsolationInfo) {
// Unlike other tests, use a TestURLLoaderFactory, which intercepts requests
// and lets their fields be examined directly, rather than a
// TestSharedURLLoaderFactory, which makes real requests. This allows directly
// inspecting the created IsolationInfo. Validating the of the IsolationInfo
// value on actual results is, unfortunately, just too difficult to be
// practical.
network::TestURLLoaderFactory url_loader_factory;
TrustedSignalsFetcher trusted_signals_fetcher;
trusted_signals_fetcher.FetchBiddingSignals(
data_decoder_manager_, &url_loader_factory, FrameTreeNodeId(),
kAuctionDevtoolsIds, kDefaultMainFrameOrigin,
network::mojom::IPAddressSpace::kLoopback, network_partition_nonce_,
GetScriptOrigin(), TrustedBiddingSignalsUrl(),
BiddingAndAuctionServerKey{
std::string(reinterpret_cast<const char*>(kTestPublicKey),
sizeof(kTestPublicKey)),
kKeyIdStr},
CreateBasicBiddingSignalsRequest(),
base::BindLambdaForTesting(
[](TrustedSignalsFetcher::SignalsFetchResult result) {
ADD_FAILURE() << "This callback should not be invoked";
}));
url_loader_factory.WaitForRequest(TrustedBiddingSignalsUrl());
ASSERT_EQ(url_loader_factory.NumPending(), 1);
const auto* request = url_loader_factory.GetPendingRequest(0);
EXPECT_EQ(request->request.url, TrustedBiddingSignalsUrl());
ASSERT_TRUE(request->request.trusted_params);
const net::IsolationInfo& isolation_info =
request->request.trusted_params->isolation_info;
EXPECT_TRUE(isolation_info.IsEqualForTesting(net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther, kDefaultMainFrameOrigin,
kDefaultMainFrameOrigin, net::SiteForCookies(),
network_partition_nonce_)));
}
// Tests that the correct IsolationInfo is used.
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsIsolationInfo) {
// Unlike other tests, use a TestURLLoaderFactory, which intercepts requests
// and lets their fields be examined directly, rather than a
// TestSharedURLLoaderFactory, which makes real requests. This allows directly
// inspecting the created IsolationInfo. Validating the of the IsolationInfo
// value on actual results is, unfortunately, just too difficult to be
// practical.
network::TestURLLoaderFactory url_loader_factory;
TrustedSignalsFetcher trusted_signals_fetcher;
trusted_signals_fetcher.FetchScoringSignals(
data_decoder_manager_, &url_loader_factory, FrameTreeNodeId(),
kAuctionDevtoolsIds, kDefaultMainFrameOrigin,
network::mojom::IPAddressSpace::kLoopback, network_partition_nonce_,
GetScriptOrigin(), TrustedScoringSignalsUrl(),
BiddingAndAuctionServerKey{
std::string(reinterpret_cast<const char*>(kTestPublicKey),
sizeof(kTestPublicKey)),
kKeyIdStr},
CreateBasicScoringSignalsRequest(),
base::BindLambdaForTesting(
[](TrustedSignalsFetcher::SignalsFetchResult result) {
ADD_FAILURE() << "This callback should not be invoked";
}));
url_loader_factory.WaitForRequest(TrustedScoringSignalsUrl());
ASSERT_EQ(url_loader_factory.NumPending(), 1);
const auto* request = url_loader_factory.GetPendingRequest(0);
EXPECT_EQ(request->request.url, TrustedScoringSignalsUrl());
ASSERT_TRUE(request->request.trusted_params);
const net::IsolationInfo& isolation_info =
request->request.trusted_params->isolation_info;
EXPECT_TRUE(isolation_info.IsEqualForTesting(net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther, kDefaultMainFrameOrigin,
kDefaultMainFrameOrigin, net::SiteForCookies(),
network_partition_nonce_)));
}
// Construct two compression groups with a total of three partitions, each
// having the same buyerTKVSignals.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsIdenticalBuyerTKVSignals) {
const std::set<std::string> kKeys;
const std::string kBuyerTKVSignals = "signal";
std::vector<TrustedSignalsFetcher::BiddingPartition> group0_partitions;
const std::set<std::string> kInterestGroupNames1{"groupA"};
group0_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames1, &kKeys,
&kDefaultAdditionalParams, &kBuyerTKVSignals);
const std::set<std::string> kInterestGroupNames2{"groupB"};
group0_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames2, &kKeys,
&kDefaultAdditionalParams, &kBuyerTKVSignals);
std::vector<TrustedSignalsFetcher::BiddingPartition> group1_partitions;
const std::set<std::string> kInterestGroupNames3{"groupC"};
group1_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames3, &kKeys,
&kDefaultAdditionalParams, &kBuyerTKVSignals);
std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>
bidding_signals_request;
bidding_signals_request.emplace(0, std::move(group0_partitions));
bidding_signals_request.emplace(1, std::move(group1_partitions));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"perPartitionMetadata": {
"contextualData": [
{
"value": "signal"
}
]
},
"partitions": [
{
"id": 0,
"arguments": [
{
"data": [ "groupA" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [ "groupB" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [ "groupC" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 1
}
]
})";
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Construct compression groups: Group 1 (partitions A, B), Group 2 (partition
// C). A and C share the same buyerTKVSignals signals; B has none.
TEST_F(TrustedSignalsFetcherTest,
BiddingSignalsPartialIdenticalBuyerTKVSignals) {
const std::set<std::string> kKeys;
const std::string kBuyerTKVSignals = "signal";
std::vector<TrustedSignalsFetcher::BiddingPartition> group0_partitions;
const std::set<std::string> kInterestGroupNames1{"groupA"};
group0_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames1, &kKeys,
&kDefaultAdditionalParams, &kBuyerTKVSignals);
const std::set<std::string> kInterestGroupNames2{"groupB"};
group0_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames2, &kKeys,
&kDefaultAdditionalParams,
/*buyer_tkv_signals=*/nullptr);
std::vector<TrustedSignalsFetcher::BiddingPartition> group1_partitions;
const std::set<std::string> kInterestGroupNames3{"groupC"};
group1_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames3, &kKeys,
&kDefaultAdditionalParams, &kBuyerTKVSignals);
std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>
bidding_signals_request;
bidding_signals_request.emplace(0, std::move(group0_partitions));
bidding_signals_request.emplace(1, std::move(group1_partitions));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"perPartitionMetadata": {
"contextualData": [
{
"ids": [
[0, 0],
[1, 0]
],
"value": "signal"
}
]
},
"partitions": [
{
"id": 0,
"arguments": [
{
"data": [ "groupA" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [ "groupB" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [ "groupC" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 1
}
]
})";
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Construct compression groups: Group 1 (partitions A, B), Group 2 (partition
// C). A and C have different buyerTKVSignals signals; B has none.
TEST_F(TrustedSignalsFetcherTest, BiddingSignalsDifferentBuyerTKVSignals) {
const std::set<std::string> kKeys;
std::vector<TrustedSignalsFetcher::BiddingPartition> group0_partitions;
const std::set<std::string> kInterestGroupNames1{"groupA"};
const std::string kBuyerTKVSignals1 = "signalA";
group0_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames1, &kKeys,
&kDefaultAdditionalParams, &kBuyerTKVSignals1);
const std::set<std::string> kInterestGroupNames2{"groupB"};
group0_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames2, &kKeys,
&kDefaultAdditionalParams,
/*buyer_tkv_signals=*/nullptr);
std::vector<TrustedSignalsFetcher::BiddingPartition> group1_partitions;
const std::set<std::string> kInterestGroupNames3{"groupC"};
const std::string kBuyerTKVSignals3 = "signalC";
group1_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames3, &kKeys,
&kDefaultAdditionalParams, &kBuyerTKVSignals3);
std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>
bidding_signals_request;
bidding_signals_request.emplace(0, std::move(group0_partitions));
bidding_signals_request.emplace(1, std::move(group1_partitions));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"perPartitionMetadata": {
"contextualData": [
{
"ids": [
[ 0, 0 ]
],
"value": "signalA"
},
{
"ids": [
[ 1, 0 ]
],
"value": "signalC"
}
]
},
"partitions": [
{
"id": 0,
"arguments": [
{
"data": [ "groupA" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [ "groupB" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [ "groupC" ],
"tags": [ "interestGroupNames" ]
},
{
"data": [],
"tags": [ "keys" ]
}
],
"compressionGroupId": 1
}
]
})";
auto result = RequestBiddingSignalsAndWaitForResult(bidding_signals_request);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Construct two compression groups with a total of three partitions, each
// having the same sellerTKVSignals.
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsIdenticalSellerTKVSignals) {
const std::set<GURL> kAdComponentRenderUrls;
const std::string kSellerTKVSignals = "signal";
std::vector<TrustedSignalsFetcher::ScoringPartition> group0_partitions;
const GURL kRenderUrl1{"https://render_urla.test/"};
group0_partitions.emplace_back(
/*partition_id=*/0, &kRenderUrl1, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, &kSellerTKVSignals);
const GURL kRenderUrl2{"https://render_urlb.test/"};
group0_partitions.emplace_back(
/*partition_id=*/1, &kRenderUrl2, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, &kSellerTKVSignals);
std::vector<TrustedSignalsFetcher::ScoringPartition> group1_partitions;
const GURL kRenderUrl3{"https://render_urlc.test/"};
group0_partitions.emplace_back(
/*partition_id=*/0, &kRenderUrl3, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, &kSellerTKVSignals);
std::map<int, std::vector<TrustedSignalsFetcher::ScoringPartition>>
scoring_signals_request;
scoring_signals_request.emplace(0, std::move(group0_partitions));
scoring_signals_request.emplace(1, std::move(group1_partitions));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"perPartitionMetadata": {
"contextualData": [
{
"value": "signal"
}
]
},
"partitions": [
{
"id": 0,
"arguments": [
{
"data": [
"https://render_urla.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 0
},
{
"id": 1,
"arguments": [
{
"data": [
"https://render_urlb.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [
"https://render_urlc.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 0
}
]
})";
auto result = RequestScoringSignalsAndWaitForResult(scoring_signals_request);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Construct compression groups: Group 1 (partitions A, B), Group 2 (partition
// C). A and C share the same sellerTKVSignals signals; B has none.
TEST_F(TrustedSignalsFetcherTest,
ScoringSignalsPartialIdenticalSellerTKVSignals) {
const std::set<GURL> kAdComponentRenderUrls;
const std::string kSellerTKVSignals = "signal";
std::vector<TrustedSignalsFetcher::ScoringPartition> group0_partitions;
const GURL kRenderUrl1{"https://render_urla.test/"};
group0_partitions.emplace_back(
/*partition_id=*/0, &kRenderUrl1, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, &kSellerTKVSignals);
const GURL kRenderUrl2{"https://render_urlb.test/"};
group0_partitions.emplace_back(
/*partition_id=*/1, &kRenderUrl2, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, /*seller_tkv_signals=*/nullptr);
std::vector<TrustedSignalsFetcher::ScoringPartition> group1_partitions;
const GURL kRenderUrl3{"https://render_urlc.test/"};
group1_partitions.emplace_back(
/*partition_id=*/0, &kRenderUrl3, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, &kSellerTKVSignals);
std::map<int, std::vector<TrustedSignalsFetcher::ScoringPartition>>
scoring_signals_request;
scoring_signals_request.emplace(0, std::move(group0_partitions));
scoring_signals_request.emplace(1, std::move(group1_partitions));
// Request body as a JSON string. Will be converted to CBOR and have a framing
// header and padding added before beign compared to actual body.
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"perPartitionMetadata": {
"contextualData": [
{
"ids": [
[ 0, 0 ],
[ 1, 0 ]
],
"value": "signal"
}
]
},
"partitions": [
{
"id": 0,
"arguments": [
{
"data": [
"https://render_urla.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 0
},
{
"id": 1,
"arguments": [
{
"data": [
"https://render_urlb.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [
"https://render_urlc.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 1
}
]
})";
auto result = RequestScoringSignalsAndWaitForResult(scoring_signals_request);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Construct compression groups: Group 1 (partitions A, B), Group 2 (partition
// C). A and C have different sellerTKVSignals signals; B has none.
TEST_F(TrustedSignalsFetcherTest, ScoringSignalsDifferentSellerTKVSignals) {
const std::set<GURL> kAdComponentRenderUrls;
std::vector<TrustedSignalsFetcher::ScoringPartition> group0_partitions;
const GURL kRenderUrl1{"https://render_urla.test/"};
const std::string kSellerTKVSignals1 = "signalA";
group0_partitions.emplace_back(
/*partition_id=*/0, &kRenderUrl1, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, &kSellerTKVSignals1);
const GURL kRenderUrl2{"https://render_urlb.test/"};
group0_partitions.emplace_back(
/*partition_id=*/1, &kRenderUrl2, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, /*seller_tkv_signals=*/nullptr);
std::vector<TrustedSignalsFetcher::ScoringPartition> group1_partitions;
const GURL kRenderUrl3{"https://render_urlc.test/"};
const std::string kSellerTKVSignals2 = "signalC";
group1_partitions.emplace_back(
/*partition_id=*/0, &kRenderUrl3, &kAdComponentRenderUrls,
&kDefaultAdditionalParams, &kSellerTKVSignals2);
std::map<int, std::vector<TrustedSignalsFetcher::ScoringPartition>>
scoring_signals_request;
scoring_signals_request.emplace(0, std::move(group0_partitions));
scoring_signals_request.emplace(1, std::move(group1_partitions));
const std::string_view kExpectedRequestBodyJson =
R"({
"acceptCompression": [ "none", "gzip" ],
"metadata": { "hostname": "host.test" },
"perPartitionMetadata": {
"contextualData": [
{
"ids": [
[ 0, 0 ]
],
"value": "signalA"
},
{
"ids": [
[ 1, 0 ]
],
"value": "signalC"
}
]
},
"partitions": [
{
"id": 0,
"arguments": [
{
"data": [
"https://render_urla.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 0
},
{
"id": 1,
"arguments": [
{
"data": [
"https://render_urlb.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 0
},
{
"id": 0,
"arguments": [
{
"data": [
"https://render_urlc.test/"
],
"tags": [
"renderURLs"
]
}
],
"compressionGroupId": 1
}
]
})";
auto result = RequestScoringSignalsAndWaitForResult(scoring_signals_request);
ValidateRequestBodyJson(kExpectedRequestBodyJson);
}
// Test that the request timeout (which should use the value of
// AuctionDownloader::kRequestTimeout) is respected. Unfortunately, can't use
// MOCK_TIME with TrustedSignalsFetcherTest test fixture, since the embedded
// test server uses its own independent thread, so the task environment may
// think it's idle and automatically advance the time while spinning the message
// loop. Even if it did use a task-environment thread, though, the platform
// socket APIs may not guarantee that socket operations occur before the task
// environment notices it has no pending events, and thus advances the time.
TEST(TrustedSignalsFetcherTimeoutTest, BiddingSignalsTimeout) {
base::test::TaskEnvironment task_environment{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
data_decoder::test::InProcessDataDecoder in_process_data_decoder;
// URLLoaderFactory that's never configured to return any results, so requests
// to it hang.
network::TestURLLoaderFactory url_loader_factory;
// None of the parameters for this test actually matter, apart from needing to
// be valid.
const GURL kSignalsUrl("https://a.test/");
const url::Origin kSignalsOrigin = url::Origin::Create(kSignalsUrl);
const std::set<std::string> kInterestGroupNames{"group1"};
const std::set<std::string> kKeys;
const base::Value::Dict kAdditionalParams;
std::vector<TrustedSignalsFetcher::BiddingPartition> bidding_partitions;
bidding_partitions.emplace_back(
/*partition_id=*/0, &kInterestGroupNames, &kKeys, &kAdditionalParams,
/*buyer_tkv_signals=*/nullptr);
std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>
bidding_signals_request;
bidding_signals_request.emplace(0, std::move(bidding_partitions));
// Start a request that should complete with a timeout error.
base::RunLoop run_loop;
DataDecoderManager data_decoder_manager;
TrustedSignalsFetcher::SignalsFetchResult out;
TrustedSignalsFetcher trusted_signals_fetcher;
trusted_signals_fetcher.FetchBiddingSignals(
data_decoder_manager, &url_loader_factory, FrameTreeNodeId(),
{"auction_devtools_id"},
/*main_frame_origin=*/kSignalsOrigin,
network::mojom::IPAddressSpace::kLoopback,
/*network_partition_nonce=*/base::UnguessableToken::Create(),
kSignalsOrigin, kSignalsUrl,
BiddingAndAuctionServerKey{
std::string(reinterpret_cast<const char*>(kTestPublicKey),
sizeof(kTestPublicKey)),
kKeyIdStr},
bidding_signals_request,
base::BindLambdaForTesting(
[&](TrustedSignalsFetcher::SignalsFetchResult result) {
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(),
base::StringPrintf(
"Failed to load %s error = net::ERR_TIMED_OUT.",
kSignalsUrl.spec().c_str()));
run_loop.Quit();
}));
constexpr base::TimeDelta kTinyTime = base::Milliseconds(1);
// Run until just before the timeout duration. The request should not time
// out.
task_environment.FastForwardBy(
auction_worklet::AuctionDownloader::kRequestTimeout - kTinyTime);
EXPECT_FALSE(run_loop.AnyQuitCalled());
// Wait until the timeout duration has passed. The request should have timed
// out.
task_environment.FastForwardBy(kTinyTime);
EXPECT_TRUE(run_loop.AnyQuitCalled());
}
} // namespace
} // namespace content