blob: 29c07d4ff6a12a6992364daf74d8364684a26e36 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/interest_group/ad_auction_service_impl.h"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "base/barrier_closure.h"
#include "base/base64.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_string_value_serializer.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/rust_buildflags.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/thread_annotations.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "components/aggregation_service/aggregation_coordinator_utils.h"
#include "components/aggregation_service/features.h"
#include "components/cbor/diagnostic_writer.h"
#include "components/cbor/reader.h"
#include "components/services/storage/shared_storage/shared_storage_manager.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/fenced_frame/fenced_frame_url_mapping.h"
#include "content/browser/interest_group/ad_auction_page_data.h"
#include "content/browser/interest_group/auction_process_manager.h"
#include "content/browser/interest_group/interest_group_caching_storage.h"
#include "content/browser/interest_group/interest_group_features.h"
#include "content/browser/interest_group/interest_group_manager_impl.h"
#include "content/browser/interest_group/interest_group_storage.h"
#include "content/browser/interest_group/storage_interest_group.h"
#include "content/browser/private_aggregation/private_aggregation_budgeter.h"
#include "content/browser/private_aggregation/private_aggregation_manager_impl.h"
#include "content/browser/private_aggregation/private_aggregation_test_utils.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/common/content_navigation_policy.h"
#include "content/common/features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/privacy_sandbox_invoking_api.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/services/auction_worklet/auction_v8_helper.h"
#include "content/services/auction_worklet/auction_worklet_service_impl.h"
#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "content/test/fenced_frame_test_utils.h"
#include "content/test/test_content_browser_client.h"
#include "crypto/sha2.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/base/isolation_info.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 "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/interest_group/interest_group.h"
#include "third_party/blink/public/common/interest_group/test_interest_group_builder.h"
#include "third_party/blink/public/common/permissions_policy/permissions_policy_features.h"
#include "third_party/blink/public/mojom/interest_group/ad_auction_service.mojom.h"
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
#include "third_party/blink/public/mojom/parakeet/ad_request.mojom.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
class BrowserContext;
namespace {
constexpr char kInterestGroupName[] = "interest-group-name";
constexpr char kOriginStringA[] = "https://a.test";
constexpr char kOriginStringB[] = "https://b.test";
constexpr char kOriginStringC[] = "https://c.test";
constexpr char kOriginStringD[] = "https://d.test";
constexpr char kOriginStringE[] = "https://e.test";
constexpr char kOriginStringF[] = "https://f.test";
constexpr char kOriginStringG[] = "https://g.test";
constexpr char kOriginStringNoUpdate[] = "https://no.update.test";
constexpr char kBiddingUrlPath[] = "/interest_group/bidding_logic.js";
constexpr char kNewBiddingUrlPath[] = "/interest_group/new_bidding_logic.js";
constexpr char kDecisionUrlPath[] = "/interest_group/decision_logic.js";
constexpr char kTrustedBiddingSignalsUrlPath[] =
"/interest_group/trusted_bidding_signals.json";
constexpr char kTrustedScoringSignalsUrlPath[] =
"/interest_group/trusted_scoring_signals.json";
constexpr char kUpdateUrlPath[] = "/interest_group/daily_update_partial.json";
constexpr char kUpdateUrlPath2[] =
"/interest_group/daily_update_partial_2.json";
constexpr char kUpdateUrlPath3[] =
"/interest_group/daily_update_partial_3.json";
constexpr char kUpdateUrlPath4[] =
"/interest_group/daily_update_partial_4.json";
constexpr char kUpdateUrlPathB[] =
"/interest_group/daily_update_partial_b.json";
constexpr char kUpdateUrlPathC[] =
"/interest_group/daily_update_partial_c.json";
constexpr char kBAndAKeyPath[] = "/interest_group/b_and_a_keys.json";
// 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,
};
// Returns kTestPublicKey as a JSON response to be returned by kBAndAKeyPath.
std::string JSONSerializedKeys() {
base::Value::Dict key;
key.Set("key", base::Base64Encode(kTestPublicKey));
key.Set("id", "12345678-9abc-def0-1234-56789abcdef0");
base::Value::List keys;
keys.Append(std::move(key));
base::Value::Dict outer;
outer.Set("keys", std::move(keys));
std::string json_output;
JSONStringValueSerializer serializer(&json_output);
serializer.Serialize(outer);
return json_output;
}
// Returns a basic bidder script that sends reports to
// kOriginStringA/report_bidder.
std::string BasicBiddingReportScript() {
return base::StringPrintf(R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin(
auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
sendReportTo('%s/report_bidder');
}
)",
kOriginStringA);
}
// Returns a basic seller script that sends reports to
// kOriginStringA/report_seller.
std::string BasicSellerReportScript() {
return base::StringPrintf(R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('%s/report_seller');
return {
'success': true,
'signalsForWinner': {'signalForWinner': 1},
'reportUrl': '%s/report_seller',
};
}
)",
kOriginStringA, kOriginStringA);
}
class AllowInterestGroupContentBrowserClient : public TestContentBrowserClient {
public:
explicit AllowInterestGroupContentBrowserClient() = default;
~AllowInterestGroupContentBrowserClient() override = default;
AllowInterestGroupContentBrowserClient(
const AllowInterestGroupContentBrowserClient&) = delete;
AllowInterestGroupContentBrowserClient& operator=(
const AllowInterestGroupContentBrowserClient&) = delete;
// ContentBrowserClient overrides:
bool IsInterestGroupAPIAllowed(content::RenderFrameHost* render_frame_host,
InterestGroupApiOperation operation,
const url::Origin& top_frame_origin,
const url::Origin& api_origin) override {
// No updating allowed on no.update.test.
if (operation == ContentBrowserClient::InterestGroupApiOperation::kUpdate &&
api_origin.host() == "no.update.test") {
return false;
}
// Can join A interest groups on A top frames, B interest groups on B top
// frames, C interest groups on C top frames, C interest groups on A top
// frames, C interest groups on D top frames, and no.update.test interest
// groups on no.update.test top frames.
return (top_frame_origin.host() == "a.test" &&
api_origin.host() == "a.test") ||
(top_frame_origin.host() == "b.test" &&
api_origin.host() == "b.test") ||
(top_frame_origin.host() == "c.test" &&
api_origin.host() == "c.test") ||
(top_frame_origin.host() == "a.test" &&
api_origin.host() == "c.test") ||
(top_frame_origin.host() == "d.test" &&
api_origin.host() == "c.test") ||
(top_frame_origin.host() == "no.update.test" &&
api_origin.host() == "no.update.test");
}
void SetAllowList(base::flat_set<url::Origin>&& allow_list) {
allow_list_ = allow_list;
}
bool IsPrivacySandboxReportingDestinationAttested(
content::BrowserContext* browser_context,
const url::Origin& destination_origin,
content::PrivacySandboxInvokingAPI invoking_api,
bool post_impression_reporting) override {
if (!allow_list_) {
return true;
}
return allow_list_->contains(destination_origin);
}
bool IsCookieDeprecationLabelAllowed(
content::BrowserContext* browser_context) override {
return true;
}
private:
// If not present, all origins are allowed.
std::optional<base::flat_set<url::Origin>> allow_list_;
};
constexpr char kFledgeUpdateHeaders[] =
"HTTP/1.1 200 OK\n"
"Content-type: Application/JSON\n"
"Ad-Auction-Allowed: true\n";
constexpr char kFledgeScriptHeaders[] =
"HTTP/1.1 200 OK\n"
"Content-type: Application/Javascript\n"
"Ad-Auction-Allowed: true\n";
constexpr char kFledgeReportHeaders[] =
"HTTP/1.1 200 OK\n"
"Ad-Auction-Allowed: true\n";
constexpr char kFledgeSignalsHeaders[] =
"HTTP/1.1 200 OK\n"
"Content-type: Application/JSON\n"
"Data-Version: 2\n"
"Ad-Auction-Allowed: true\n";
// Allows registering network responses to update and scoring / bidding script
// requests; *must* be destroyed before the task environment is shutdown (which
// happens in RenderViewHostTestHarness::TearDown()).
//
// Updates and script serving have different requirements, but unfortunately
// it's not possible to simultaneously instantiate 2 classes that both use their
// own URLLoaderInterceptor...so these are combined in this same class.
class NetworkResponder {
public:
using NetCallback =
base::RepeatingCallback<void(URLLoaderInterceptor::RequestParams*)>;
using SignalsCallback =
base::RepeatingCallback<void(URLLoaderInterceptor::RequestParams*)>;
// Register interest group update `response` to be served with JSON
// content type when a request to `url_path` is made.
void RegisterUpdateResponse(const std::string& url_path,
const std::string& response) {
base::AutoLock auto_lock(lock_);
json_update_map_[url_path] = response;
}
// Register script `response` to be served with Javascript content type when a
// request to `url_path` is made.
void RegisterScriptResponse(const std::string& url_path,
const std::string& response) {
base::AutoLock auto_lock(lock_);
script_map_[url_path] = response;
}
// Register ad auction reporting `response` to be served when a request to
// `url_path` is made.
void RegisterReportResponse(const std::string& url_path,
const std::string& response) {
base::AutoLock auto_lock(lock_);
report_map_[url_path] = response;
}
// Register signals `response` to be served when a request to `url_path` is
// made.
void RegisterSignalsResponse(const std::string& url_path,
SignalsCallback callback) {
base::AutoLock auto_lock(lock_);
signals_map_[url_path] = std::move(callback);
}
// Register a repeat callback to be served when a request to `url_path` is
// made.
void RegisterRepeatCallback(const std::string& url_path,
const NetCallback callback) {
base::AutoLock auto_lock(lock_);
net_callback_map_[url_path] = callback;
}
// Registers a URL to use a "deferred" script response. For a deferred
// response, the request handler returns true without a write, and writes are
// performed later in DoDeferred[Script|Update]Write() using a "stolen" Mojo
// pipe to the URLLoaderClient.
//
// It is valid to have a "deferred" response that never completes before the
// test exits.
void RegisterDeferredScriptResponse(const std::string& url_path) {
RegisterDeferredResponse(url_path, /*is_update=*/false);
}
// Just like RegisterDeferredResponse(), but for deferred update responses.
void RegisterDeferredUpdateResponse(const std::string& url_path) {
RegisterDeferredResponse(url_path, /*is_update=*/true);
}
// Checks if there's a deferred response (of any type) for `url_path`.
bool HasPendingResponse(const std::string& url_path) const {
base::AutoLock auto_lock(lock_);
auto it = deferred_responses_map_.find(url_path);
CHECK(it != deferred_responses_map_.end());
return it->second.url_loader_client.is_bound() &&
it->second.url_loader_client.is_connected();
}
// Perform the deferred response for `url_path`, using response headers
// associated with scripts -- the test fails if the client isn't waiting on
// `url_path` registered with RegisterDeferredResponse().
void DoDeferredScriptResponse(const std::string& url_path,
const std::string& response) {
DoDeferredResponse(url_path, response, kFledgeScriptHeaders,
/*expected_is_update=*/false);
}
// Perform the deferred response for `url_path`, using response headers
// associated with updates -- the test fails if the client isn't waiting on
// `url_path` registered with RegisterDeferredResponse().
void DoDeferredUpdateResponse(const std::string& url_path,
const std::string& response) {
DoDeferredResponse(url_path, response, kFledgeScriptHeaders,
/*expected_is_update=*/true);
}
// Registers a URL that, when seen, will have its URLLoaderClient stored in
// `stored_url_loader_client_` without sending a response.
//
// Only one request can be handled with this method at a time.
void RegisterStoreUrlLoaderClient(const std::string& url_path) {
base::AutoLock auto_lock(lock_);
store_url_loader_client_url_path_ = url_path;
}
// Make the next request fail with `error` -- subsequent requests will succeed
// again unless another FailNextUpdateRequestWithError() call is made.
//
// TODO(crbug.com/1298593): Replace this with FailUpdateRequestWithError().
void FailNextUpdateRequestWithError(net::Error error) {
base::AutoLock auto_lock(lock_);
update_next_error_ = error;
}
// Like FailNextUpdateRequestWithError(), but for a specific path.
void FailUpdateRequestWithError(const std::string& path, net::Error error) {
base::AutoLock auto_lock(lock_);
update_error_ = error;
update_error_path_ = path;
}
// Like FailUpdateRequestWithError(), but doesn't alter the update count or
// expect transient NIKs.
void FailRequestWithError(const std::string& path, net::Error error) {
base::AutoLock auto_lock(lock_);
non_update_error_ = error;
non_update_error_path_ = path;
}
// Returns the number of updates that occurred -- does not include other
// network requests.
size_t UpdateCount() const {
base::AutoLock auto_lock(lock_);
return update_count_;
}
// Returns the number of reports that occurred -- does not include other
// network requests.
size_t ReportCount() const {
base::AutoLock auto_lock(lock_);
return report_count_;
}
// Returns true if the network request for path received a response.
bool ReportSent(const std::string& path) const {
base::AutoLock auto_lock(lock_);
return base::Contains(sent_reports_, path);
}
// Indicates whether `stored_url_loader_client_` is connected to a receiver.
bool RemoteIsConnected() {
base::AutoLock auto_lock(lock_);
return stored_url_loader_client_.is_connected();
}
void WaitForNumReports(size_t num_reports) {
base::RunLoop run_loop;
{
base::AutoLock auto_lock(lock_);
DCHECK(!quit_report_wait_loop_callback_);
EXPECT_LE(report_count_, num_reports);
if (report_count_ >= num_reports) {
return;
}
waiting_for_report_count_ = num_reports;
quit_report_wait_loop_callback_ = run_loop.QuitClosure();
}
run_loop.Run();
{
base::AutoLock auto_lock(lock_);
waiting_for_report_count_ = 0;
EXPECT_EQ(report_count_, num_reports);
}
}
private:
bool RequestHandler(URLLoaderInterceptor::RequestParams* params) {
base::AutoLock auto_lock(lock_);
// Check if there is a registered repeat callback.
const auto callback_it =
net_callback_map_.find(params->url_request.url.path());
if (callback_it != net_callback_map_.end()) {
callback_it->second.Run(params);
}
// Check deferred responses map.
const auto deferred_it =
deferred_responses_map_.find(params->url_request.url.path());
if (deferred_it != deferred_responses_map_.end()) {
CHECK(!deferred_it->second.url_loader_client);
deferred_it->second.url_loader_client = std::move(params->client);
if (deferred_it->second.is_update) {
OnUpdateRequestReceived(params);
}
return true;
}
// Cross-origin iframe handling is covered by integration tests, for cases
// that request .well-known URLs.
if (params->url_request.url.path_piece() ==
"/.well-known/interest-group/permissions/") {
CHECK(false);
return false;
}
// Check if this is a non-update error.
if (params->url_request.url.path() == non_update_error_path_) {
CHECK(non_update_error_ != net::OK);
params->client->OnComplete(
network::URLLoaderCompletionStatus(non_update_error_));
return true;
}
// Not a non-update error, check if this is a script request.
const auto script_it = script_map_.find(params->url_request.url.path());
if (script_it != script_map_.end()) {
URLLoaderInterceptor::WriteResponse(
kFledgeScriptHeaders, script_it->second, params->client.get());
return true;
}
// Not a non-update error or script request, check if it's a reporting
// request.
const auto report_it = report_map_.find(params->url_request.url.path());
if (report_it != report_map_.end()) {
URLLoaderInterceptor::WriteResponse(
kFledgeReportHeaders, report_it->second, params->client.get());
sent_reports_.push_back(params->url_request.url.path());
OnReportSent();
return true;
}
// Check if it's a trusted bidding/scoring signals response.
const auto signals_it = signals_map_.find(params->url_request.url.path());
if (signals_it != signals_map_.end()) {
signals_it->second.Run(params);
return true;
}
if ((params->url_request.url.path() == store_url_loader_client_url_path_)) {
CHECK(!stored_url_loader_client_);
stored_url_loader_client_ = std::move(params->client);
OnReportSent();
return true;
}
// Not a non-update error, script request, or report request, so consider
// this an update request.
OnUpdateRequestReceived(params);
const auto update_it =
json_update_map_.find(params->url_request.url.path());
if (update_it != json_update_map_.end()) {
URLLoaderInterceptor::WriteResponse(
kFledgeUpdateHeaders, update_it->second, params->client.get());
return true;
}
if (params->url_request.url.path() == update_error_path_) {
CHECK(update_error_ != net::OK);
params->client->OnComplete(
network::URLLoaderCompletionStatus(update_error_));
return true;
}
if (update_next_error_ != net::OK) {
params->client->OnComplete(
network::URLLoaderCompletionStatus(update_next_error_));
update_next_error_ = net::OK;
return true;
}
return false;
}
void OnReportSent() EXCLUSIVE_LOCKS_REQUIRED(lock_) {
++report_count_;
if (waiting_for_report_count_ == report_count_) {
std::move(quit_report_wait_loop_callback_).Run();
}
}
void OnUpdateRequestReceived(URLLoaderInterceptor::RequestParams* params)
EXCLUSIVE_LOCKS_REQUIRED(lock_) {
update_count_++;
EXPECT_TRUE(params->url_request.trusted_params);
EXPECT_TRUE(params->url_request.trusted_params->isolation_info
.network_isolation_key()
.IsTransient());
}
void RegisterDeferredResponse(const std::string& url_path, bool is_update) {
base::AutoLock auto_lock(lock_);
CHECK(deferred_responses_map_
.emplace(url_path,
DeferredResponseInfo{
mojo::Remote<network::mojom::URLLoaderClient>(),
is_update})
.second);
}
void DoDeferredResponse(const std::string& url_path,
const std::string& response,
const std::string headers,
bool expected_is_update) {
base::AutoLock auto_lock(lock_);
auto it = deferred_responses_map_.find(url_path);
CHECK(it != deferred_responses_map_.end());
EXPECT_EQ(expected_is_update, it->second.is_update);
mojo::Remote<network::mojom::URLLoaderClient>& url_loader_client =
it->second.url_loader_client;
CHECK(url_loader_client.is_bound());
URLLoaderInterceptor::WriteResponse(headers, response,
url_loader_client.get());
deferred_responses_map_.erase(it);
}
// Handles network requests for interest group updates and scripts.
URLLoaderInterceptor network_interceptor_{
base::BindRepeating(&NetworkResponder::RequestHandler,
base::Unretained(this))};
mutable base::Lock lock_;
// For each HTTPS request, we see if any path in the map matches the request
// path. If so, the server returns the mapped value string as the response,
// with JSON MIME type.
base::flat_map<std::string, std::string> json_update_map_ GUARDED_BY(lock_);
// Like `json_update_map_`, but for serving bidding / scoring scripts, with
// the Javascript MIME type.
base::flat_map<std::string, std::string> script_map_ GUARDED_BY(lock_);
// Like `json_update_map_`, but for reporting requests.
base::flat_map<std::string, std::string> report_map_ GUARDED_BY(lock_);
// Like `json_update_map_`, but for registered callbacks that will be given
// the `URLLoaderInterceptor::RequestParams*` and return the response. Used
// for trusted signals requests.
base::flat_map<std::string, SignalsCallback> signals_map_ GUARDED_BY(lock_);
// Like `json_update_map_`, but for registered callbacks that will be given
// the `URLLoaderInterceptor::RequestParams*`.
base::flat_map<std::string, NetCallback> net_callback_map_ GUARDED_BY(lock_);
// Only saves reporting requests that auctually received responses.
std::vector<std::string> sent_reports_ GUARDED_BY(lock_);
struct DeferredResponseInfo {
mojo::Remote<network::mojom::URLLoaderClient> url_loader_client;
bool is_update = false;
};
// Stores the set of URL paths that will receive a deferred response.
//
// First, a URL path is registered to defer the response, but the mapped value
// will not be bound.
//
// Next, once a request is made for that URL path, the
// URLLoaderClient used for the request is stored as the value for that URL
// path.
//
// Finally, after the deferred response is made, the key-value pair for that
// response is removed from the map.
//
// It is valid to have a "deferred" response that never completes before the
// test exits.
base::flat_map<std::string, DeferredResponseInfo> deferred_responses_map_
GUARDED_BY(lock_);
// Stores the last URL path that was registered with
// RegisterStoreUrlLoaderClient().
std::string store_url_loader_client_url_path_ GUARDED_BY(lock_);
// Stores the Mojo URLLoaderClient remote "stolen" from
// RequestHandlerForUpdates() for use with no responses -- unbound if no
// remote has been "stolen" yet, or if the last no response request timed out.
mojo::Remote<network::mojom::URLLoaderClient> stored_url_loader_client_
GUARDED_BY(lock_);
// For updates, fail the next request with `update_next_error_` if
// `update_next_error_` is not net::OK.
net::Error update_next_error_ GUARDED_BY(lock_) = net::OK;
// For updates, the error to return if `update_error_path_` matches the path
// of the current request.
net::Error update_error_ GUARDED_BY(lock_) = net::OK;
// For updates, if the current request's path matches `update_error_path_`,
// fail the request with `update_error_`.
std::string update_error_path_ GUARDED_BY(lock_);
// The non-update variant doesn't alter the update attempt counter or check
// for transient NIKs.
// For non-updates, the error to return if `update_error_path_` matches the
// path of the current request.
net::Error non_update_error_ GUARDED_BY(lock_) = net::OK;
// For non-updates, if the current request's path matches
// `update_error_path_`, fail the request with `update_error_`.
std::string non_update_error_path_ GUARDED_BY(lock_);
size_t update_count_ GUARDED_BY(lock_) = 0;
size_t report_count_ GUARDED_BY(lock_) = 0;
// Used to wait for a specific number of reports.
size_t waiting_for_report_count_ GUARDED_BY(lock_) = 0;
base::OnceClosure quit_report_wait_loop_callback_ GUARDED_BY(lock_);
};
// AuctionProcessManager that allows running auctions in-proc.
class SameProcessAuctionProcessManager : public AuctionProcessManager {
public:
SameProcessAuctionProcessManager() = default;
SameProcessAuctionProcessManager(const SameProcessAuctionProcessManager&) =
delete;
SameProcessAuctionProcessManager& operator=(
const SameProcessAuctionProcessManager&) = delete;
~SameProcessAuctionProcessManager() override = default;
private:
RenderProcessHost* LaunchProcess(
mojo::PendingReceiver<auction_worklet::mojom::AuctionWorkletService>
auction_worklet_service_receiver,
const ProcessHandle* handle,
const std::string& display_name) override {
// Create one AuctionWorkletServiceImpl per Mojo pipe, just like in
// production code. Don't bother to delete the service on pipe close,
// though; just keep it in a vector instead.
auction_worklet_services_.push_back(
auction_worklet::AuctionWorkletServiceImpl::CreateForService(
std::move(auction_worklet_service_receiver)));
return nullptr;
}
scoped_refptr<SiteInstance> MaybeComputeSiteInstance(
SiteInstance* frame_site_instance,
const url::Origin& worklet_origin) override {
return nullptr;
}
bool TryUseSharedProcess(ProcessHandle* process_handle) override {
return false;
}
std::vector<std::unique_ptr<auction_worklet::AuctionWorkletServiceImpl>>
auction_worklet_services_;
};
class TestKAnonymityServiceDelegate : public KAnonymityServiceDelegate {
public:
TestKAnonymityServiceDelegate() = default;
void JoinSet(std::string id,
base::OnceCallback<void(bool)> callback) override {
join_ids_.push_back(id);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), true));
}
void QuerySets(
std::vector<std::string> ids,
base::OnceCallback<void(std::vector<bool>)> callback) override {
// Return that nothing is k-anonymous.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback),
std::vector<bool>(ids.size(), false)));
}
base::TimeDelta GetJoinInterval() override { return base::Seconds(1); }
base::TimeDelta GetQueryInterval() override { return base::Seconds(1); }
const std::vector<std::string>& join_ids() const { return join_ids_; }
private:
std::vector<std::string> join_ids_;
};
} // namespace
// Tests the interest group management functionality of AdAuctionServiceImpl --
// this particular functionality used to be in a separate interface called
// RestrictedInterestStore. The interfaces were combined so so that they'd share
// a Mojo pipe (for message ordering consistency).
class AdAuctionServiceImplTest : public RenderViewHostTestHarness {
public:
AdAuctionServiceImplTest()
: RenderViewHostTestHarness(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
feature_list_.InitWithFeatures(
/*enabled_features=*/
{blink::features::kInterestGroupStorage,
blink::features::kAdInterestGroupAPI, blink::features::kFledge,
blink::features::kFledgeClearOriginJoinedAdInterestGroups,
blink::features::kFledgeNegativeTargeting,
blink::features::kPrivateAggregationApiMultipleCloudProviders,
aggregation_service::kAggregationServiceMultipleCloudProviders,
features::kEnableUpdatingUserBiddingSignals,
features::kEnableUpdatingExecutionModeToFrozenContext},
/*disabled_features=*/{});
fenced_frame_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
old_content_browser_client_ =
SetBrowserClientForTesting(&content_browser_client_);
}
~AdAuctionServiceImplTest() override {
SetBrowserClientForTesting(old_content_browser_client_);
}
void SetUp() override {
RenderViewHostTestHarness::SetUp();
NavigateAndCommit(kUrlA);
manager_ = static_cast<InterestGroupManagerImpl*>(
browser_context()
->GetDefaultStoragePartition()
->GetInterestGroupManager());
// Process creation crashes in the Chrome zygote init in unit tests, so run
// the auction "processes" in-process instead.
manager_->set_auction_process_manager_for_testing(
std::make_unique<SameProcessAuctionProcessManager>());
manager_->set_k_anonymity_manager_for_testing(
std::make_unique<InterestGroupKAnonymityManager>(
manager_.get(),
base::BindLambdaForTesting([&]() -> KAnonymityServiceDelegate* {
return &k_anon_delegate_;
})));
}
void TearDown() override {
// `network_responder_` must be destructed while the task environment,
// which gets destroyed by RenderViewHostTestHarness::TearDown(), is still
// active.
network_responder_.reset();
manager_ = nullptr; // avoid dangling.
RenderViewHostTestHarness::TearDown();
}
scoped_refptr<StorageInterestGroups> GetInterestGroupsForOwner(
const url::Origin& owner) {
scoped_refptr<StorageInterestGroups> interest_groups;
base::RunLoop run_loop;
manager_->GetInterestGroupsForOwner(
/*devtools_auction_id=*/std::nullopt, owner,
base::BindLambdaForTesting(
[&run_loop,
&interest_groups](scoped_refptr<StorageInterestGroups> groups) {
interest_groups = std::move(groups);
run_loop.Quit();
}));
run_loop.Run();
return interest_groups;
}
// Returns the specified interest group, if it exists.
std::optional<SingleStorageInterestGroup> GetInterestGroup(
const url::Origin& owner,
const std::string& name) {
scoped_refptr<StorageInterestGroups> igs = GetInterestGroupsForOwner(owner);
for (const SingleStorageInterestGroup& interest_group :
igs->GetInterestGroups()) {
if (interest_group->interest_group.name == name) {
return interest_group;
}
}
return std::nullopt;
}
int GetJoinCount(const url::Origin& owner, const std::string& name) {
std::optional<SingleStorageInterestGroup> interest_group(
GetInterestGroup(owner, name));
if (!interest_group) {
return 0;
}
return interest_group.value()->bidding_browser_signals->join_count;
}
double GetPriority(const url::Origin& owner, const std::string& name) {
std::optional<SingleStorageInterestGroup> interest_group(
GetInterestGroup(owner, name));
if (!interest_group) {
return 0;
}
return interest_group.value()->interest_group.priority;
}
// Retrieves the FencedFrameProperties for the specified URN from the main
// frame. Returns nullopt if no such URN exists.
std::optional<FencedFrameProperties> GetFencedFramePropertiesForURN(
const GURL& urn_url) {
TestFencedFrameURLMappingResultObserver observer;
FencedFrameURLMapping& fenced_frame_urls_map =
static_cast<RenderFrameHostImpl*>(main_rfh())
->GetPage()
.fenced_frame_urls_map();
fenced_frame_urls_map.ConvertFencedFrameURNToURL(urn_url, &observer);
return observer.fenced_frame_properties();
}
std::optional<GURL> ConvertFencedFrameURNToURL(const GURL& urn_url) {
auto properties = GetFencedFramePropertiesForURN(urn_url);
if (properties && properties->mapped_url().has_value()) {
return properties->mapped_url()->GetValueIgnoringVisibility();
}
return std::nullopt;
}
// Invokes the callback for the provided URN in the scope of the main frame,
// as would happen if it were navigated to. Doesn't use NavigationSimulator
// because it doesn't seem capable of triggering URN swaps. Integration tests
// cover actual swap cases.
void InvokeCallbackForURN(const GURL& urn_url) {
auto properties = GetFencedFramePropertiesForURN(urn_url);
ASSERT_TRUE(properties);
properties->on_navigate_callback().Run();
}
// Creates a new AdAuctionServiceImpl and use it to try and join
// `interest_group`. Waits for the operation to signal completion.
//
// Creates a new AdAuctionServiceImpl with each call so the RFH can be
// navigated between different sites. And AdAuctionServiceImpl only handles
// one site (cross site navs use different AdAuctionServices, and generally
// use different RFHs as well).
//
// If `rfh` is nullptr, uses the main frame.
void JoinInterestGroupAndFlush(const blink::InterestGroup& interest_group,
RenderFrameHost* rfh = nullptr) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
rfh ? rfh : main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
interest_service->JoinInterestGroup(
interest_group,
base::BindLambdaForTesting(
[&](bool failed_well_known_check) { run_loop.Quit(); }));
run_loop.Run();
// Pipe should not have been closed - if it is expected to be closed, use
// JoinInterestGroupAndExpectBadMessage().
EXPECT_TRUE(interest_service.is_bound());
EXPECT_TRUE(interest_service.is_connected());
}
// Attempts to join an interest group and expects the pipe to be closed and
// the passed in bad message Mojo error to be recorded. This happens when an
// operation should have been rejected in the renderer, so should only happen
// if the renderer has been compromised.
//
// If `rfh` is nullptr, uses the main frame.
void JoinInterestGroupAndExpectBadMessage(
const blink::InterestGroup& interest_group,
std::string_view expected_bad_message,
RenderFrameHost* rfh = nullptr) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
rfh ? rfh : main_rfh(), interest_service.BindNewPipeAndPassReceiver());
mojo::test::BadMessageObserver observer;
base::RunLoop run_loop;
interest_service.set_disconnect_handler(run_loop.QuitClosure());
interest_service->JoinInterestGroup(
interest_group, base::BindOnce([](bool failed_well_known_check) {
ADD_FAILURE() << "This callback should not be invoked.";
}));
run_loop.Run();
EXPECT_EQ(expected_bad_message, observer.WaitForBadMessage());
}
// Analogous to JoinInterestGroupAndFlush(), but leaves an interest
// group instead of joining one.
void LeaveInterestGroupAndFlush(const url::Origin& owner,
const std::string& name,
RenderFrameHost* rfh = nullptr) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
rfh ? rfh : main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
interest_service->LeaveInterestGroup(
owner, name,
base::BindLambdaForTesting(
[&](bool failed_well_known_check) { run_loop.Quit(); }));
run_loop.Run();
// Pipe should not have been closed - if it is expected to be closed, use
// LeaveInterestGroupAndExpectBadMessage().
EXPECT_TRUE(interest_service.is_bound());
EXPECT_TRUE(interest_service.is_connected());
}
// Analogous to JoinInterestGroupAndExpectBadMessage(), but leaves an interest
// group instead of joining one.
void LeaveInterestGroupAndExpectBadMessage(
const url::Origin& owner,
const std::string& name,
std::string_view expected_bad_message,
RenderFrameHost* rfh = nullptr) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
rfh ? rfh : main_rfh(), interest_service.BindNewPipeAndPassReceiver());
mojo::test::BadMessageObserver observer;
base::RunLoop run_loop;
interest_service.set_disconnect_handler(run_loop.QuitClosure());
interest_service->LeaveInterestGroup(
owner, name, base::BindOnce([](bool failed_well_known_check) {
ADD_FAILURE() << "This callback should not be invoked.";
}));
run_loop.Run();
EXPECT_EQ(expected_bad_message, observer.WaitForBadMessage());
}
// Calls ClearOriginJoinedInterestGroups() with the provided parameters, and
// expects the pipe to be closed and the passed in bad message Mojo error to
// be recorded.
void ClearOriginJoinedInterestGroupsAndExpectBadMessage(
const url::Origin& owner,
const std::string_view expected_bad_message,
RenderFrameHost* rfh = nullptr) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
rfh ? rfh : main_rfh(), interest_service.BindNewPipeAndPassReceiver());
mojo::test::BadMessageObserver observer;
base::RunLoop run_loop;
interest_service.set_disconnect_handler(run_loop.QuitClosure());
interest_service->ClearOriginJoinedInterestGroups(
owner, /*interest_groups_to_keep=*/{},
base::BindOnce([](bool failed_well_known_check) {
ADD_FAILURE() << "This callback should not be invoked.";
}));
run_loop.Run();
EXPECT_EQ(expected_bad_message, observer.WaitForBadMessage());
}
// Updates registered interest groups according to their registered update
// URL. Doesn't flush since the update operation requires a sequence of
// asynchronous operations.
void UpdateInterestGroupNoFlushForFrame(RenderFrameHost* rfh) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
rfh, interest_service.BindNewPipeAndPassReceiver());
interest_service->UpdateAdInterestGroups();
}
// Runs an ad auction using the config specified in `auction_config` in the
// frame `rfh`. Returns the result of the auction, which is either a URL to
// the winning ad, or std::nullopt if no ad won the auction.
std::optional<GURL> RunAdAuctionAndFlushForFrame(
const blink::AuctionConfig& auction_config,
RenderFrameHost* rfh) {
// Use a new service for each call. Keep the service alive as some calls
// (e.g., sending reports via the URN callback) require it not be deleted.
ad_auction_service_.reset();
AdAuctionServiceImpl::CreateMojoService(
rfh, ad_auction_service_.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
std::optional<blink::FencedFrame::RedactedFencedFrameConfig> maybe_config;
ad_auction_service_->RunAdAuction(
auction_config, mojo::NullReceiver(),
base::BindLambdaForTesting(
[&run_loop, &maybe_config](
bool aborted_by_script,
const std::optional<
blink::FencedFrame::RedactedFencedFrameConfig>& config) {
EXPECT_FALSE(aborted_by_script);
maybe_config = config;
run_loop.Quit();
}));
ad_auction_service_.FlushForTesting();
run_loop.Run();
if (!maybe_config) {
return std::nullopt;
}
CHECK(maybe_config->urn_uuid().has_value());
return maybe_config->urn_uuid();
}
// Like RunAdAuctionAndFlushForFrame(), but uses the RenderFrameHost of the
// main frame.
std::optional<GURL> RunAdAuctionAndFlush(
const blink::AuctionConfig& auction_config) {
return RunAdAuctionAndFlushForFrame(auction_config, main_rfh());
}
// Like UpdateInterestGroupNoFlushForFrame, but uses the RenderFrameHost of
// the main frame.
void UpdateInterestGroupNoFlush() {
UpdateInterestGroupNoFlushForFrame(main_rfh());
}
// Helper to create a valid interest group with only an origin and name. All
// URLs are nullopt.
blink::InterestGroup CreateInterestGroup() {
blink::InterestGroup interest_group;
interest_group.expiry = base::Time::Now() + base::Seconds(300);
interest_group.name = kInterestGroupName;
interest_group.owner = kOriginA;
return interest_group;
}
void CreateAdRequest(blink::mojom::AdRequestConfigPtr config,
AdAuctionServiceImpl::CreateAdRequestCallback callback) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
interest_service->CreateAdRequest(std::move(config), std::move(callback));
interest_service.FlushForTesting();
}
// Finalizes an ad and expects the Mojo pipe to be closed without invoking the
// callback, as should be done in the case of a bad Mojo message.
void FinalizeAdAndExpectPipeClosed(const std::string& guid,
const blink::AuctionConfig& config) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
interest_service.set_disconnect_handler(run_loop.QuitClosure());
interest_service->FinalizeAd(
guid, config,
base::BindLambdaForTesting(
[&](const std::optional<GURL>& creative_url) {
ADD_FAILURE() << "Callback unexpectedly invoked.";
}));
run_loop.Run();
}
// Destroy the AdAuctionService, if there is one.
void DestroyAdAuctionService() { ad_auction_service_.reset(); }
const std::vector<std::string>& GetKAnonJoinedIds() const {
return k_anon_delegate_.join_ids();
}
protected:
const GURL kUrlA = GURL(kOriginStringA);
const url::Origin kOriginA = url::Origin::Create(kUrlA);
const GURL kUrlB = GURL(kOriginStringB);
const url::Origin kOriginB = url::Origin::Create(kUrlB);
const GURL kUrlC = GURL(kOriginStringC);
const url::Origin kOriginC = url::Origin::Create(kUrlC);
const GURL kUrlD = GURL(kOriginStringD);
const url::Origin kOriginD = url::Origin::Create(kUrlD);
const GURL kUrlE = GURL(kOriginStringE);
const url::Origin kOriginE = url::Origin::Create(kUrlE);
const GURL kUrlF = GURL(kOriginStringF);
const url::Origin kOriginF = url::Origin::Create(kUrlF);
const GURL kUrlG = GURL(kOriginStringG);
const url::Origin kOriginG = url::Origin::Create(kUrlG);
const GURL kUrlNoUpdate = GURL(kOriginStringNoUpdate);
const url::Origin kOriginNoUpdate = url::Origin::Create(kUrlNoUpdate);
const GURL kBiddingLogicUrlA = kUrlA.Resolve(kBiddingUrlPath);
const GURL kNewBiddingLogicUrlA = kUrlA.Resolve(kNewBiddingUrlPath);
const GURL kTrustedBiddingSignalsUrlA =
kUrlA.Resolve(kTrustedBiddingSignalsUrlPath);
const GURL kTrustedScoringSignalsUrlA =
kUrlA.Resolve(kTrustedScoringSignalsUrlPath);
const GURL kUpdateUrlA = kUrlA.Resolve(kUpdateUrlPath);
const GURL kUpdateUrlA2 = kUrlA.Resolve(kUpdateUrlPath2);
const GURL kUpdateUrlA3 = kUrlA.Resolve(kUpdateUrlPath3);
const GURL kUpdateUrlA4 = kUrlA.Resolve(kUpdateUrlPath4);
const GURL kUpdateUrlB = kUrlB.Resolve(kUpdateUrlPathB);
const GURL kUpdateUrlC = kUrlC.Resolve(kUpdateUrlPathC);
const GURL kUpdateUrlNoUpdate = kUrlNoUpdate.Resolve(kUpdateUrlPath);
base::test::ScopedFeatureList feature_list_;
base::test::ScopedFeatureList fenced_frame_feature_list_;
AllowInterestGroupContentBrowserClient content_browser_client_;
TestKAnonymityServiceDelegate k_anon_delegate_;
raw_ptr<ContentBrowserClient> old_content_browser_client_ = nullptr;
raw_ptr<InterestGroupManagerImpl> manager_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
// Must be destroyed before RenderViewHostTestHarness::TearDown().
std::unique_ptr<NetworkResponder> network_responder_{
std::make_unique<NetworkResponder>()};
mojo::Remote<blink::mojom::AdAuctionService> ad_auction_service_;
};
// Check basic success case.
TEST_F(AdAuctionServiceImplTest, JoinInterestGroupBasic) {
blink::InterestGroup interest_group = CreateInterestGroup();
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Several tests assume interest group API are also allowed on kOriginB, so
// make sure that's enabled correctly.
NavigateAndCommit(kUrlB);
interest_group.owner = kOriginB;
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginB, kInterestGroupName));
}
// Non-HTTPS frames should not be able to join interest groups.
TEST_F(AdAuctionServiceImplTest, JoinInterestGroupFrameNotHttps) {
// Note that the ContentBrowserClient allows URLs based on hosts, not origins,
// so it should not block this URL. Instead, it should run into the HTTPS
// check.
const GURL kHttpUrlA = GURL("http://a.test/");
const url::Origin kHttpOriginA = url::Origin::Create(kHttpUrlA);
NavigateAndCommit(kHttpUrlA);
// Try to join an HTTPS interest group.
blink::InterestGroup interest_group = CreateInterestGroup();
JoinInterestGroupAndExpectBadMessage(
interest_group,
"Unexpected request: Interest groups may only be joined or left from "
"https origins");
EXPECT_EQ(0, GetJoinCount(interest_group.owner, kInterestGroupName));
// Try to join a same-origin HTTP interest group.
interest_group.owner = kHttpOriginA;
JoinInterestGroupAndExpectBadMessage(
interest_group,
"Validation failed for blink.mojom.AdAuctionService.4 "
"[VALIDATION_ERROR_DESERIALIZATION_FAILED]");
EXPECT_EQ(0, GetJoinCount(kHttpOriginA, kInterestGroupName));
}
// Try to join a non-HTTPS interest group.
TEST_F(AdAuctionServiceImplTest, JoinInterestGroupOwnerNotHttps) {
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = url::Origin::Create(GURL("http://a.test/"));
JoinInterestGroupAndExpectBadMessage(
interest_group,
"Validation failed for blink.mojom.AdAuctionService.4 "
"[VALIDATION_ERROR_DESERIALIZATION_FAILED]");
EXPECT_EQ(0, GetJoinCount(interest_group.owner, kInterestGroupName));
// Secure, but not HTTPS.
interest_group.owner = url::Origin::Create(GURL("wss://a.test/"));
JoinInterestGroupAndExpectBadMessage(
interest_group,
"Validation failed for blink.mojom.AdAuctionService.4 "
"[VALIDATION_ERROR_DESERIALIZATION_FAILED]");
EXPECT_EQ(0, GetJoinCount(interest_group.owner, kInterestGroupName));
}
// Test joining an interest group with a disallowed URL. Doesn't
// exhaustively test all cases, as the validation function has its own unit
// tests. This is just to make sure those are hooked up.
TEST_F(AdAuctionServiceImplTest, JoinInterestGroupDisallowedUrls) {
const GURL kBadUrl = GURL("https://user:pass@a.test/");
// Test `bidding_url`.
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kBadUrl;
JoinInterestGroupAndExpectBadMessage(
interest_group,
"Validation failed for blink.mojom.AdAuctionService.4 "
"[VALIDATION_ERROR_DESERIALIZATION_FAILED]");
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
// Test `update_url`.
interest_group = CreateInterestGroup();
interest_group.update_url = kBadUrl;
JoinInterestGroupAndExpectBadMessage(
interest_group,
"Validation failed for blink.mojom.AdAuctionService.4 "
"[VALIDATION_ERROR_DESERIALIZATION_FAILED]");
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
// Test `trusted_bidding_signals_url`.
interest_group = CreateInterestGroup();
interest_group.trusted_bidding_signals_url = kBadUrl;
JoinInterestGroupAndExpectBadMessage(
interest_group,
"Validation failed for blink.mojom.AdAuctionService.4 "
"[VALIDATION_ERROR_DESERIALIZATION_FAILED]");
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
}
// Successful join. Duplicate allowed reporting origins are removed.
TEST_F(AdAuctionServiceImplTest,
JoinInterestGroupDeduplicateAllowedReportingOrigins) {
content_browser_client_.SetAllowList({kOriginF, kOriginG});
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.ads.emplace();
std::vector<url::Origin> allowed_reporting_origins = {kOriginG, kOriginF,
kOriginG};
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*ad_render_id=*/std::nullopt,
/*allowed_reporting_origins=*/std::move(allowed_reporting_origins));
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
auto group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
std::optional<std::vector<int>> x = {{1, 2, 3}};
EXPECT_THAT(x.value(), ::testing::UnorderedElementsAre(1, 2, 3));
EXPECT_THAT(group.ads.value()[0].allowed_reporting_origins.value(),
::testing::UnorderedElementsAre(kOriginF, kOriginG));
}
// Attempt to join an interest group whose allowed reporting origins are not all
// attested. No join should happen.
TEST_F(AdAuctionServiceImplTest,
JoinInterestGroupNotAttestedAllowedReportingOrigins) {
const url::Origin kNotAttestedOrigin =
url::Origin::Create(GURL("https://a.test"));
content_browser_client_.SetAllowList({kOriginG});
// Test `bidding_url`.
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.ads.emplace();
std::vector<url::Origin> allowed_reporting_origins = {kOriginG,
kNotAttestedOrigin};
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*ad_render_id=*/std::nullopt,
/*allowed_reporting_origins=*/std::move(allowed_reporting_origins));
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
}
// Attempt to join an interest group whose size is very large. No join should
// happen -- it should fail and close the pipe.
TEST_F(AdAuctionServiceImplTest, JoinMassiveInterestGroupFails) {
blink::InterestGroup interest_group = CreateInterestGroup();
// 1 MiB of '5' characters is over the size limit.
interest_group.user_bidding_signals = std::string(1024 * 1024, '5');
JoinInterestGroupAndExpectBadMessage(
interest_group,
"Validation failed for blink.mojom.AdAuctionService.4 "
"[VALIDATION_ERROR_DESERIALIZATION_FAILED]");
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 0u);
}
// Trying to leave Non-HTTPS interest groups should not be possible, and result
// in the pipe being closed. Can't check there's an HTTP group that isn't left,
// since it should be impossible to join one in the first place.
TEST_F(AdAuctionServiceImplTest, LeaveClearInterestGroupOriginNotHttps) {
const GURL kHttpUrl = GURL("http://a.test/");
const url::Origin kHttpOrigin = url::Origin::Create(kHttpUrl);
NavigateAndCommit(kUrlA);
LeaveInterestGroupAndExpectBadMessage(
kHttpOrigin, kInterestGroupName,
"Unexpected request: Interest groups may only be owned by https origins");
ClearOriginJoinedInterestGroupsAndExpectBadMessage(
kHttpOrigin,
"Unexpected request: Interest groups may only be owned by https origins");
}
// Non-HTTPS interest origins should not be able to leave groups should be
// rejected, and result in the pipe being closed.
TEST_F(AdAuctionServiceImplTest, LeaveClearInterestGroupFrameNotHttps) {
const GURL kHttpUrl = GURL("http://a.test/");
const url::Origin kHttpOrigin = url::Origin::Create(kHttpUrl);
NavigateAndCommit(kUrlA);
JoinInterestGroupAndFlush(CreateInterestGroup());
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Navigate to an HTTP origin and try to leave a group with an HTTPS owner.
// The request should be rejected.
NavigateAndCommit(kHttpUrl);
LeaveInterestGroupAndExpectBadMessage(
kOriginA, kInterestGroupName,
"Unexpected request: Interest groups may only be joined or left from "
"https origins");
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Clearing shouldn't leave the IG, even if it's incorrectly executed, since
// the joining origin is wrong, but still make sure there's no effect, just in
// case.
ClearOriginJoinedInterestGroupsAndExpectBadMessage(
kOriginA,
"Unexpected request: Interest groups may only be joined or left from "
"https origins");
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
}
TEST_F(AdAuctionServiceImplTest, FixExpiryOnJoin) {
const base::TimeDelta kMaxExpiry = base::Days(30);
blink::InterestGroup interest_group = CreateInterestGroup();
// Join an interest group with an expiry that's exactly the maximum allowed.
// The expiry should be stored without modification.
interest_group.expiry = base::Time::Now() + kMaxExpiry;
JoinInterestGroupAndFlush(interest_group);
{
std::optional<SingleStorageInterestGroup> storage_interest_group(
GetInterestGroup(kOriginA, kInterestGroupName));
ASSERT_TRUE(storage_interest_group.has_value());
EXPECT_EQ(
1, storage_interest_group.value()->bidding_browser_signals->join_count);
EXPECT_EQ(interest_group.expiry,
storage_interest_group.value()->interest_group.expiry);
}
// Rejoin the interest group with a short expiry. The expiry should also be
// stored without modification.
interest_group.expiry = base::Time::Now() + base::Days(1);
JoinInterestGroupAndFlush(interest_group);
{
std::optional<SingleStorageInterestGroup> storage_interest_group(
GetInterestGroup(kOriginA, kInterestGroupName));
ASSERT_TRUE(storage_interest_group.has_value());
EXPECT_EQ(
2, storage_interest_group.value()->bidding_browser_signals->join_count);
EXPECT_EQ(interest_group.expiry,
storage_interest_group.value()->interest_group.expiry);
}
// Rejoin the interest group with an expiry that exceeds the maximum allowed.
// The expiry should be set to kMaxExpiry days from now.
interest_group.expiry = base::Time::Now() + base::Days(300);
JoinInterestGroupAndFlush(interest_group);
{
std::optional<SingleStorageInterestGroup> storage_interest_group(
GetInterestGroup(kOriginA, kInterestGroupName));
ASSERT_TRUE(storage_interest_group.has_value());
EXPECT_EQ(
3, storage_interest_group.value()->bidding_browser_signals->join_count);
base::Time actual_expiry =
storage_interest_group.value()->interest_group.expiry;
EXPECT_EQ(base::Time::Now() + kMaxExpiry, actual_expiry);
EXPECT_NE(interest_group.expiry, actual_expiry);
}
}
// These tests validate the `updateURL` and navigator.updateAdInterestGroups()
// functionality.
// The server JSON updates all fields that can be updated.
TEST_F(AdAuctionServiceImplTest, UpdateAllUpdatableFields) {
content_browser_client_.SetAllowList({kOriginF, kOriginG});
// TODO(caraitto): Remove camelCase sellerCapabilities fields when no longer
// supported.
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath,
base::StringPrintf(
R"({
"priority": 1.59,
"enableBiddingSignalsPrioritization": true,
"priorityVector": {"old1": 2, "new1": 1.1},
"prioritySignalsOverrides": {"old2": 1, "new1": 1.1,
"browserSignals.reserved":-1},
"sellerCapabilities": {"%s": ["latency-stats"],
"*": ["interest-group-counts", "latencyStats"]},
"biddingLogicURL": "%s/interest_group/new_bidding_logic.js",
"biddingWasmHelperUrl":"%s/interest_group/new_bidding_wasm_helper_url.wasm",
"trustedBiddingSignalsURL":
"%s/interest_group/new_trusted_bidding_signals_url.json",
"trustedBiddingSignalsKeys": ["new_key"],
"trustedBiddingSignalsSlotSizeMode": "slot-size",
"maxTrustedBiddingSignalsURLLength": 8000,
"userBiddingSignals": {"test":10},
"updateURL": "%s/interest_group/new_daily_update_partial.json",
"ads": [{"renderURL": "%s/new_ad_render_url",
"sizeGroup": "group_new",
"metadata": {"new_a": "b"},
"buyerReportingId": "new_brid",
"buyerAndSellerReportingId": "new_shrid",
"adRenderId": "123abc",
"allowedReportingOrigins":
["https://g.test", "https://f.test", "https://g.test"]
}],
"adComponents": [{"renderURL": "https://example.com/component_url",
"sizeGroup": "group_new",
"metadata": {"new_c": "d"},
"buyerReportingId": "ignored1",
"buyerAndSellerReportingId": "ignored2",
"adRenderId": "456def",
"allowedReportingOrigins": ["https://ignored.test"]
}],
"adSizes": {"size_new": {"width": "300px", "height": "150px"}},
"sizeGroups": {"group_new": ["size_new"]},
"auctionServerRequestFlags": ["omit-ads", "include-full-ads"],
"privateAggregationConfig": {
"aggregationCoordinatorOrigin": "%s"
}
})",
kOriginStringA, kOriginStringA, kOriginStringA, kOriginStringA,
kOriginStringA, kOriginStringA,
aggregation_service::kDefaultAggregationCoordinatorAwsCloud));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.priority = 2.0;
interest_group.enable_bidding_signals_prioritization = false;
interest_group.priority_vector = {{{"old1", 1}, {"old2", 2}}};
interest_group.priority_signals_overrides = {{{"old1", 1}, {"old2", 2}}};
interest_group.seller_capabilities.emplace();
interest_group.seller_capabilities->insert(std::make_pair(
kOriginA, blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts})));
interest_group.all_sellers_capabilities = {
blink::SellerCapabilities::kLatencyStats};
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.trusted_bidding_signals_slot_size_mode = blink::InterestGroup::
TrustedBiddingSignalsSlotSizeMode::kAllSlotsRequestedSizes;
interest_group.user_bidding_signals.emplace();
interest_group.user_bidding_signals = "{\"test\":4}";
interest_group.max_trusted_bidding_signals_url_length = 10000;
interest_group.ads.emplace();
std::vector<url::Origin> allowed_reporting_origins = {kOriginF};
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/"{\"ad\":\"metadata\",\"here\":[1,2,3]}",
/*size_group=*/"group_old",
/*buyer_reporting_id=*/"old_brid",
/*buyer_and_seller_reporting_id=*/"old_shrid",
/*ad_render_id=*/"123abc",
/*allowed_reporting_origins=*/std::move(allowed_reporting_origins));
interest_group.ads->emplace_back(std::move(ad));
interest_group.ad_components.emplace();
blink::InterestGroup::Ad ad_component(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/"{\"ad\":\"metadata\",\"here\":[1,2,3]}",
/*size_group=*/"group_old",
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt,
/*ad_render_id=*/"123def");
interest_group.ad_components->emplace_back(std::move(ad_component));
interest_group.ad_sizes.emplace();
interest_group.ad_sizes->emplace(
"size_old", blink::AdSize(640, blink::AdSize::LengthUnit::kPixels, 480,
blink::AdSize::LengthUnit::kPixels));
interest_group.size_groups.emplace();
std::vector<std::string> size_list = {"size_old"};
interest_group.size_groups->emplace("group_old", size_list);
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
EXPECT_EQ(group.priority, 1.59);
EXPECT_EQ(group.enable_bidding_signals_prioritization, true);
// The new value for `priority_vector` should completely replace the old one.
base::flat_map<std::string, double> expected_priority_vector{{"old1", 2},
{"new1", 1.1}};
EXPECT_EQ(group.priority_vector, expected_priority_vector);
// The new value for `priority_signals_overrides` should be merged with the
// old one. Interest groups can use the "browserSignals." prefix, though it's
// not allowed in auctionConfig.prioritySignals fields.
base::flat_map<std::string, double> expected_priority_signals_overrides{
{"old1", 1}, {"old2", 1}, {"new1", 1.1}, {"browserSignals.reserved", -1}};
EXPECT_EQ(group.priority_signals_overrides,
expected_priority_signals_overrides);
EXPECT_EQ(group.all_sellers_capabilities,
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats}));
ASSERT_TRUE(group.seller_capabilities);
ASSERT_EQ(group.seller_capabilities->size(), 1u);
EXPECT_EQ(group.seller_capabilities->at(kOriginA),
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kLatencyStats}));
ASSERT_TRUE(group.bidding_url.has_value());
EXPECT_EQ(group.bidding_url->spec(),
base::StringPrintf("%s/interest_group/new_bidding_logic.js",
kOriginStringA));
ASSERT_TRUE(group.bidding_wasm_helper_url.has_value());
EXPECT_EQ(
group.bidding_wasm_helper_url->spec(),
base::StringPrintf("%s/interest_group/new_bidding_wasm_helper_url.wasm",
kOriginStringA));
ASSERT_TRUE(group.trusted_bidding_signals_url.has_value());
EXPECT_EQ(group.trusted_bidding_signals_url->spec(),
base::StringPrintf(
"%s/interest_group/new_trusted_bidding_signals_url.json",
kOriginStringA));
ASSERT_TRUE(group.trusted_bidding_signals_keys.has_value());
EXPECT_EQ(group.trusted_bidding_signals_keys->size(), 1u);
EXPECT_EQ(group.trusted_bidding_signals_keys.value()[0], "new_key");
EXPECT_EQ(group.trusted_bidding_signals_slot_size_mode,
blink::InterestGroup::TrustedBiddingSignalsSlotSizeMode::kSlotSize);
EXPECT_EQ(group.max_trusted_bidding_signals_url_length, 8000);
ASSERT_TRUE(group.user_bidding_signals.has_value());
EXPECT_EQ(group.user_bidding_signals.value(), "{\"test\":10}");
ASSERT_TRUE(group.update_url.has_value());
EXPECT_EQ(
group.update_url->spec(),
base::StringPrintf("%s/interest_group/new_daily_update_partial.json",
kOriginStringA));
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
base::StringPrintf("%s/new_ad_render_url", kOriginStringA));
EXPECT_EQ(group.ads.value()[0].size_group, "group_new");
EXPECT_EQ(group.ads.value()[0].metadata, "{\"new_a\":\"b\"}");
ASSERT_TRUE(group.ads.value()[0].buyer_reporting_id.has_value());
EXPECT_EQ(*group.ads.value()[0].buyer_reporting_id, "new_brid");
ASSERT_TRUE(group.ads.value()[0].buyer_and_seller_reporting_id.has_value());
EXPECT_EQ(*group.ads.value()[0].buyer_and_seller_reporting_id, "new_shrid");
ASSERT_TRUE(group.ads.value()[0].allowed_reporting_origins.has_value());
EXPECT_THAT(group.ads.value()[0].allowed_reporting_origins.value(),
::testing::UnorderedElementsAre(kOriginF, kOriginG));
ASSERT_TRUE(group.ad_components.has_value());
ASSERT_EQ(group.ad_components->size(), 1u);
EXPECT_EQ(group.ad_components.value()[0].render_url(),
"https://example.com/component_url");
EXPECT_EQ(group.ad_components.value()[0].size_group, "group_new");
EXPECT_EQ(group.ad_components.value()[0].metadata, "{\"new_c\":\"d\"}");
EXPECT_FALSE(group.ad_components.value()[0].buyer_reporting_id.has_value());
EXPECT_FALSE(
group.ad_components.value()[0].buyer_and_seller_reporting_id.has_value());
EXPECT_FALSE(
group.ad_components.value()[0].allowed_reporting_origins.has_value());
ASSERT_TRUE(group.ad_components.value()[0].ad_render_id.has_value());
EXPECT_EQ(group.ad_components.value()[0].ad_render_id.value(), "456def");
ASSERT_TRUE(group.ad_sizes.has_value());
ASSERT_EQ(group.ad_sizes->size(), 1u);
EXPECT_EQ(group.ad_sizes->at("size_new"),
blink::AdSize(300, blink::AdSize::LengthUnit::kPixels, 150,
blink::AdSize::LengthUnit::kPixels));
ASSERT_TRUE(group.size_groups.has_value());
ASSERT_EQ(group.size_groups->size(), 1u);
EXPECT_EQ(group.size_groups->at("group_new")[0], "size_new");
EXPECT_TRUE(group.auction_server_request_flags.Has(
blink::AuctionServerRequestFlagsEnum::kOmitAds));
EXPECT_TRUE(group.auction_server_request_flags.Has(
blink::AuctionServerRequestFlagsEnum::kIncludeFullAds));
EXPECT_EQ(
aggregation_service::kDefaultAggregationCoordinatorAwsCloud,
group.aggregation_coordinator_origin.value_or(url::Origin()).Serialize());
}
TEST_F(AdAuctionServiceImplTest, UpdateExecutionModeToGroupByOrigin) {
base::HistogramTester histogram_tester;
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"executionMode": "group-by-origin"
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.execution_mode =
blink::mojom::InterestGroup_ExecutionMode::kFrozenContext;
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
EXPECT_EQ(groups->GetInterestGroups().at(0)->interest_group.execution_mode,
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Update.AuctionExecutionMode",
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode, 1);
}
TEST_F(AdAuctionServiceImplTest, UpdateExecutionModeToFrozenContext) {
base::HistogramTester histogram_tester;
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"executionMode": "frozen-context"
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.execution_mode =
blink::mojom::InterestGroup_ExecutionMode::kGroupedByOriginMode;
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
EXPECT_EQ(groups->GetInterestGroups().at(0)->interest_group.execution_mode,
blink::InterestGroup::ExecutionMode::kFrozenContext);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Update.AuctionExecutionMode",
blink::InterestGroup::ExecutionMode::kFrozenContext, 1);
}
TEST_F(AdAuctionServiceImplTest, UpdateExecutionModeTocompatibilityMode) {
base::HistogramTester histogram_tester;
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"executionMode": "compatibility"
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.execution_mode =
blink::mojom::InterestGroup_ExecutionMode::kFrozenContext;
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
EXPECT_EQ(groups->GetInterestGroups().at(0)->interest_group.execution_mode,
blink::InterestGroup::ExecutionMode::kCompatibilityMode);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Update.AuctionExecutionMode",
blink::InterestGroup::ExecutionMode::kCompatibilityMode, 1);
}
TEST_F(AdAuctionServiceImplTest,
UpdateUnrecognizedExecutionModeToCompatibility) {
base::HistogramTester histogram_tester;
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"executionMode": "unrecognized-mode"
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.execution_mode =
blink::mojom::InterestGroup_ExecutionMode::kGroupedByOriginMode;
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
EXPECT_EQ(groups->GetInterestGroups().at(0)->interest_group.execution_mode,
blink::InterestGroup::ExecutionMode::kCompatibilityMode);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Update.AuctionExecutionMode",
blink::InterestGroup::ExecutionMode::kCompatibilityMode, 1);
}
class AdAuctionServiceImplUpdateExecutionModeToFrozenContextDisabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplUpdateExecutionModeToFrozenContextDisabledTest() {
feature_list_.InitAndDisableFeature(
features::kEnableUpdatingExecutionModeToFrozenContext);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplUpdateExecutionModeToFrozenContextDisabledTest,
DisabledUpdateExecutionModeToFrozenContext) {
base::HistogramTester histogram_tester;
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"executionMode": "frozen-context"
})");
blink::InterestGroup interest_group1 = CreateInterestGroup();
interest_group1.update_url = kUpdateUrlA;
interest_group1.bidding_url = kBiddingLogicUrlA;
interest_group1.execution_mode =
blink::mojom::InterestGroup_ExecutionMode::kCompatibilityMode;
JoinInterestGroupAndFlush(interest_group1);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.execution_mode,
blink::mojom::InterestGroup_ExecutionMode::kCompatibilityMode);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Update.AuctionExecutionMode",
blink::InterestGroup::ExecutionMode::kCompatibilityMode, 1);
}
// Only set the ads field -- the other fields shouldn't be changed.
TEST_F(AdAuctionServiceImplTest, UpdatePartialPerformsMerge) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"ads": [{"renderURL": "%s/new_ad_render_url",
"metadata": {"new_a": "b"}
}]
})",
kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.priority = 2.0;
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.trusted_bidding_signals_slot_size_mode = blink::InterestGroup::
TrustedBiddingSignalsSlotSizeMode::kAllSlotsRequestedSizes;
interest_group.max_trusted_bidding_signals_url_length = 10000;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/"{\"ad\":\"metadata\",\"here\":[1,2,3]}");
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
EXPECT_EQ(group.priority, 2.0);
ASSERT_TRUE(group.bidding_url.has_value());
EXPECT_EQ(
group.bidding_url->spec(),
base::StringPrintf("%s/interest_group/bidding_logic.js", kOriginStringA));
ASSERT_TRUE(group.update_url.has_value());
EXPECT_EQ(group.update_url->spec(),
base::StringPrintf("%s/interest_group/daily_update_partial.json",
kOriginStringA));
ASSERT_TRUE(group.trusted_bidding_signals_url.has_value());
EXPECT_EQ(group.trusted_bidding_signals_url->spec(),
base::StringPrintf("%s/interest_group/trusted_bidding_signals.json",
kOriginStringA));
ASSERT_TRUE(group.trusted_bidding_signals_keys.has_value());
EXPECT_EQ(group.trusted_bidding_signals_keys->size(), 1u);
EXPECT_EQ(group.trusted_bidding_signals_keys.value()[0], "key1");
EXPECT_EQ(interest_group.trusted_bidding_signals_slot_size_mode,
blink::InterestGroup::TrustedBiddingSignalsSlotSizeMode::
kAllSlotsRequestedSizes);
EXPECT_EQ(interest_group.max_trusted_bidding_signals_url_length, 10000);
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
base::StringPrintf("%s/new_ad_render_url", kOriginStringA));
EXPECT_EQ(group.ads.value()[0].metadata, "{\"new_a\":\"b\"}");
}
// The update shouldn't change the expiration time of the interest group.
TEST_F(AdAuctionServiceImplTest, UpdateDoesntChangeExpiration) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Lookup expiry from the database before updating.
scoped_refptr<StorageInterestGroups> groups_before_update =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups_before_update->size(), 1u);
const base::Time kExpirationTime =
groups_before_update->GetInterestGroups()[0]->interest_group.expiry;
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The expiration time shouldn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
EXPECT_EQ(group.expiry, kExpirationTime);
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Updates should succeed even when updating interest groups with no ads.
TEST_F(AdAuctionServiceImplTest, UpdateGroupWithNoAds) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"trustedBiddingSignalsURL":
"%s/interest_group/new_trusted_bidding_signals_url.json",
"trustedBiddingSignalsKeys": ["new_key"]
})",
kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
ASSERT_TRUE(group.bidding_url.has_value());
EXPECT_EQ(
group.bidding_url->spec(),
base::StringPrintf("%s/interest_group/bidding_logic.js", kOriginStringA));
ASSERT_TRUE(group.update_url.has_value());
EXPECT_EQ(group.update_url->spec(),
base::StringPrintf("%s/interest_group/daily_update_partial.json",
kOriginStringA));
ASSERT_TRUE(group.trusted_bidding_signals_url.has_value());
EXPECT_EQ(group.trusted_bidding_signals_url->spec(),
base::StringPrintf(
"%s/interest_group/new_trusted_bidding_signals_url.json",
kOriginStringA));
ASSERT_TRUE(group.trusted_bidding_signals_keys.has_value());
EXPECT_EQ(group.trusted_bidding_signals_keys->size(), 1u);
EXPECT_EQ(group.trusted_bidding_signals_keys.value()[0], "new_key");
EXPECT_FALSE(group.ads.has_value());
}
// Only set the ads field -- the other fields shouldn't be changed.
TEST_F(AdAuctionServiceImplTest, UpdateSucceedsIfOptionalNameOwnerMatch) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath,
base::StringPrintf(R"({
"name": "%s",
"owner": "%s",
"ads": [{"renderURL": "%s/new_ad_render_url"
}]
})",
kInterestGroupName, kOriginStringA, kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
ASSERT_TRUE(group.bidding_url.has_value());
EXPECT_EQ(
group.bidding_url->spec(),
base::StringPrintf("%s/interest_group/bidding_logic.js", kOriginStringA));
ASSERT_TRUE(group.update_url.has_value());
EXPECT_EQ(group.update_url->spec(),
base::StringPrintf("%s/interest_group/daily_update_partial.json",
kOriginStringA));
ASSERT_TRUE(group.trusted_bidding_signals_url.has_value());
EXPECT_EQ(group.trusted_bidding_signals_url->spec(),
base::StringPrintf("%s/interest_group/trusted_bidding_signals.json",
kOriginStringA));
ASSERT_TRUE(group.trusted_bidding_signals_keys.has_value());
EXPECT_EQ(group.trusted_bidding_signals_keys->size(), 1u);
EXPECT_EQ(group.trusted_bidding_signals_keys.value()[0], "key1");
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
base::StringPrintf("%s/new_ad_render_url", kOriginStringA));
}
// An unrecognized trustedBiddingSignalsSlotSizeMode should be treated as if it
// were "none".
TEST_F(AdAuctionServiceImplTest,
UnrecognizedTrustedBiddingSignalsSlotSizeMode) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath,
R"({"trustedBiddingSignalsSlotSizeMode": "non-standard value"})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.trusted_bidding_signals_slot_size_mode =
blink::InterestGroup::TrustedBiddingSignalsSlotSizeMode::kSlotSize;
interest_group.ads.emplace();
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
EXPECT_EQ(group.trusted_bidding_signals_slot_size_mode,
blink::InterestGroup::TrustedBiddingSignalsSlotSizeMode::kNone);
}
// For forward compatibility we should silently ignore fields that we don't
// know about.
TEST_F(AdAuctionServiceImplTest, UpdateIgnoresUnknownFields) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"unsupportedField": "InInterestGroup",
"ads": [{
"renderURL": "https://example.com/new_render",
"unsupportedField": "InAd"
}],
"adComponents": [{
"renderURL": "https://example.com/new_component",
"unsupportedField": "InAdComponent"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ad changed.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
ASSERT_EQ(group.ad_components->size(), 1u);
EXPECT_EQ(group.ad_components.value()[0].render_url(),
"https://example.com/new_component");
}
// Try to set the name -- for security, name and owner shouldn't be
// allowed to change. If they don't match the interest group (update URLs are
// registered per interest group), fail the update and don't update anything.
TEST_F(AdAuctionServiceImplTest, NoUpdateIfOptionalNameDoesntMatch) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"name": "boats",
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
}
// Try to set the owner -- for security, name and owner shouldn't be
// allowed to change. If they don't match the interest group (update URLs are
// registered per interest group), fail the update and don't update anything.
TEST_F(AdAuctionServiceImplTest, NoUpdateIfOptionalOwnerDoesntMatch) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"owner": "%s",
"ads": [{"renderURL": "%s/new_ad_render_url"
}]
})",
kOriginStringB, kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
}
TEST_F(AdAuctionServiceImplTest, UpdatePriorityVector) {
// These are all set in sequence, on top of each other, so if one update
// should fail to parse, the previous value should be unmodified.
const struct {
const char* priority_vector_value;
base::flat_map<std::string, double> expected_priority_vector;
} kTestCases[] = {
// Set one value.
{R"({"key1":1})", {{"key1", 1}}},
// Overwrite it.
{R"({"key1":2})", {{"key1", 2}}},
// Trying to set a value that's not a double should fail.
{R"({"key1":null})", {{"key1", 2}}},
{R"({"key1":"42"})", {{"key1", 2}}},
{R"({"key1":[42]})", {{"key1", 2}}},
// Setting the entire vector to something that isn't a dict should fail.
{R"(null)", {{"key1", 2}}},
{R"([])", {{"key1", 2}}},
{R"(5)", {{"key1", 2}}},
// Old values should not be preserved when setting new values, even when
// not explicitly overwriting the old key.
{R"({"key2":-2,"key3":0})", {{"key2", -2}, {"key3", 0}}},
// Empty value is valid.
{R"({})", {}},
};
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.expiry = base::Time::Now() + base::Days(30);
JoinInterestGroupAndFlush(interest_group);
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
EXPECT_EQ(groups->GetInterestGroups()[0]->interest_group.priority_vector,
std::nullopt);
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.priority_vector_value);
// Pass enough time so that update rate limits don't cause an update to
// fail.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod);
// Set new update response, and update.
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({"priorityVector": %s})",
test_case.priority_vector_value));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.priority_vector, test_case.expected_priority_vector);
}
}
TEST_F(AdAuctionServiceImplTest, UpdatePrioritySignalsOverrides) {
// These are all set in sequence, on top of each other, so if one update
// should fail to parse, the previous value should be unmodified.
const struct {
const char* priority_signals_overrides_value;
base::flat_map<std::string, double> expected_priority_signals_overrides;
} kTestCases[] = {
// Set one value.
{R"({"key1":1})", {{"key1", 1}}},
// Overwrite it.
{R"({"key1":2})", {{"key1", 2}}},
// Trying to set a value that's not a double or null should fail.
{R"({"key1":"42"})", {{"key1", 2}}},
{R"({"key1":[42]})", {{"key1", 2}}},
// Setting the entire vector to something that isn't a dict should fail.
{R"(null)", {{"key1", 2}}},
{R"([])", {{"key1", 2}}},
{R"(5)", {{"key1", 2}}},
// New values should be merged with old values.
{R"({"key2":-2,"key3":0})", {{"key1", 2}, {"key2", -2}, {"key3", 0}}},
// Setting a value to null should delete it.
{R"({"key2":null})", {{"key1", 2}, {"key3", 0}}},
// Empty value is valid, but has no effect.
{R"({})", {{"key1", 2}, {"key3", 0}}},
};
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.expiry = base::Time::Now() + base::Days(30);
JoinInterestGroupAndFlush(interest_group);
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
EXPECT_EQ(
groups->GetInterestGroups()[0]->interest_group.priority_signals_overrides,
std::nullopt);
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.priority_signals_overrides_value);
// Pass enough time so that update rate limits don't cause an update to
// fail.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod);
// Set new update response, and update.
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath,
base::StringPrintf(R"({"prioritySignalsOverrides": %s})",
test_case.priority_signals_overrides_value));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
EXPECT_EQ(groups->GetInterestGroups()[0]
->interest_group.priority_signals_overrides,
test_case.expected_priority_signals_overrides);
}
}
// Join 2 interest groups, each with the same owner, but with different update
// URLs. Both interest groups should be updated correctly.
TEST_F(AdAuctionServiceImplTest, UpdateMultipleInterestGroups) {
constexpr char kGroupName1[] = "group1";
constexpr char kGroupName2[] = "group2";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render1"}]
})");
network_responder_->RegisterUpdateResponse(kUpdateUrlPath2, R"({
"ads": [{"renderURL": "https://example.com/new_render2"}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.update_url = kUrlA.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kGroupName1));
// Now, join the second interest group, also belonging to `kOriginA`.
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupName2;
interest_group_2.update_url = kUrlA.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kBiddingLogicUrlA;
interest_group_2.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group_2.trusted_bidding_signals_keys.emplace();
interest_group_2.trusted_bidding_signals_keys->push_back("key1");
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2);
EXPECT_EQ(1, GetJoinCount(kOriginA, kGroupName2));
// Now, run the update. Both interest groups should update.
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Both interest groups should update.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 2u);
const auto& first_group =
groups->GetInterestGroups()[0]->interest_group.name == kGroupName1
? groups->GetInterestGroups()[0]->interest_group
: groups->GetInterestGroups()[1]->interest_group;
const auto& second_group =
groups->GetInterestGroups()[0]->interest_group.name == kGroupName2
? groups->GetInterestGroups()[0]->interest_group
: groups->GetInterestGroups()[1]->interest_group;
EXPECT_EQ(first_group.name, kGroupName1);
ASSERT_TRUE(first_group.ads.has_value());
ASSERT_EQ(first_group.ads->size(), 1u);
EXPECT_EQ(first_group.ads.value()[0].render_url(),
"https://example.com/new_render1");
EXPECT_EQ(second_group.name, kGroupName2);
ASSERT_TRUE(second_group.ads.has_value());
ASSERT_EQ(second_group.ads->size(), 1u);
EXPECT_EQ(second_group.ads.value()[0].render_url(),
"https://example.com/new_render2");
}
class AdAuctionServiceImplDifferentNIKDuringUpdateTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplDifferentNIKDuringUpdateTest() {
feature_list_.InitAndEnableFeature(features::kGroupNIKByJoiningOrigin);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Join two interest groups with two different owners but one joining origin.
// Check if they reuse same isolation info.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateCheckNIKForTwoOwnersOneJoiningOrigin) {
constexpr char kGroupNameA[] = "groupA";
constexpr char kGroupNameC[] = "groupC";
net::IsolationInfo isolation_info1;
net::IsolationInfo isolation_info2;
base::RunLoop run_loop1;
base::RunLoop run_loop2;
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath,
base::BindLambdaForTesting(
[&isolation_info1,
&run_loop1](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info1 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop1.Quit();
}));
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath2,
base::BindLambdaForTesting(
[&isolation_info2,
&run_loop2](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info2 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop2.Quit();
}));
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlA to the top frame kUrlA.
// Create and update an interest group owned by kOriginA.
content::RenderFrameHost* subframeA = rfh_tester->AppendChild("subframeA");
subframeA =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlA, subframeA);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupNameA;
interest_group.update_url = kUrlA.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframeA);
EXPECT_EQ(1, GetJoinCount(kOriginA, kGroupNameA));
UpdateInterestGroupNoFlushForFrame(subframeA);
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframeC = rfh_tester->AppendChild("subframeC");
subframeC =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframeC);
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupNameC;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframeC);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupNameC));
UpdateInterestGroupNoFlushForFrame(subframeC);
run_loop1.Run();
run_loop2.Run();
task_environment()->RunUntilIdle();
ASSERT_TRUE(isolation_info1.IsEqualForTesting(isolation_info2));
}
// Join two interest groups with one owner but two different joining origins.
// Check if they use different isolation info.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateCheckNIKForOneOwnerTwoJoiningOrigins) {
constexpr char kGroupName1[] = "group1";
constexpr char kGroupName2[] = "group2";
net::IsolationInfo isolation_info1;
net::IsolationInfo isolation_info2;
base::RunLoop run_loop1;
base::RunLoop run_loop2;
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath,
base::BindLambdaForTesting(
[&isolation_info1,
&run_loop1](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info1 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop1.Quit();
}));
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath2,
base::BindLambdaForTesting(
[&isolation_info2,
&run_loop2](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info2 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop2.Quit();
}));
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester_1 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_1 = rfh_tester_1->AppendChild("subframe1");
subframe_1 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_1);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.owner = kOriginC;
interest_group.update_url = kUrlC.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName1));
UpdateInterestGroupNoFlushForFrame(subframe_1);
// Navigate to top frame kUrlC.
NavigateAndCommit(kUrlC);
content::RenderFrameHostTester* rfh_tester_2 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlC.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_2 = rfh_tester_2->AppendChild("subframe2");
subframe_2 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_2);
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupName2;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName2));
UpdateInterestGroupNoFlushForFrame(subframe_2);
run_loop1.Run();
run_loop2.Run();
ASSERT_FALSE(isolation_info1.IsEqualForTesting(isolation_info2));
}
// Join two interest groups with two different owners but one joining origin.
// Ensure the second one join after all updating work completes and the queue is
// popped empty. Check that the isolation info is different.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdatePopOwnerQueueToEmptyTriggerClearIsolationMap) {
constexpr char kGroupNameA[] = "groupA";
constexpr char kGroupNameC[] = "groupC";
net::IsolationInfo isolation_info1;
net::IsolationInfo isolation_info2;
base::RunLoop run_loop1;
base::RunLoop run_loop2;
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath,
base::BindLambdaForTesting(
[&isolation_info1,
&run_loop1](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info1 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop1.Quit();
}));
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath2,
base::BindLambdaForTesting(
[&isolation_info2,
&run_loop2](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info2 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop2.Quit();
}));
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlA to the top frame kUrlA.
// Create and update an interest group owned by kOriginA.
content::RenderFrameHost* subframeA = rfh_tester->AppendChild("subframeA");
subframeA =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlA, subframeA);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupNameA;
interest_group.update_url = kUrlA.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframeA);
EXPECT_EQ(1, GetJoinCount(kOriginA, kGroupNameA));
UpdateInterestGroupNoFlushForFrame(subframeA);
run_loop1.Run();
// Ensure the update process is done for the first interest group.
task_environment()->RunUntilIdle();
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframeC = rfh_tester->AppendChild("subframeC");
subframeC =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframeC);
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupNameC;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframeC);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupNameC));
UpdateInterestGroupNoFlushForFrame(subframeC);
run_loop2.Run();
ASSERT_FALSE(isolation_info1.IsEqualForTesting(isolation_info2));
}
// Join two interest groups with two different owners but one joining origin.
// Ensure the owner queue gets cleared before the second interest group join.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateClearOwnerQueueTriggerClearIsolationMap) {
constexpr char kGroupNameA[] = "groupA";
constexpr char kGroupNameC[] = "groupC";
net::IsolationInfo isolation_info1;
net::IsolationInfo isolation_info2;
base::RunLoop run_loop1;
base::RunLoop run_loop2;
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath,
base::BindLambdaForTesting(
[&isolation_info1,
&run_loop1](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info1 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop1.Quit();
}));
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath2,
base::BindLambdaForTesting(
[&isolation_info2,
&run_loop2](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info2 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop2.Quit();
}));
// Disconnect the network during the update process and use network
// disconnected error to trigger the clear owner queue action.
network_responder_->FailUpdateRequestWithError(
kUpdateUrlPath, net::ERR_INTERNET_DISCONNECTED);
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlA to the top frame kUrlA.
// Create and update an interest group owned by kOriginA.
content::RenderFrameHost* subframeA = rfh_tester->AppendChild("subframeA");
subframeA =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlA, subframeA);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupNameA;
interest_group.update_url = kUrlA.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframeA);
EXPECT_EQ(1, GetJoinCount(kOriginA, kGroupNameA));
UpdateInterestGroupNoFlushForFrame(subframeA);
run_loop1.Run();
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframeC = rfh_tester->AppendChild("subframeC");
subframeC =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframeC);
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupNameC;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframeC);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupNameC));
UpdateInterestGroupNoFlushForFrame(subframeC);
run_loop2.Run();
ASSERT_FALSE(isolation_info1.IsEqualForTesting(isolation_info2));
}
// If there are two groups with joining origin A, two groups with joining origin
// C, and batch limitation size as three, it cannot mix different joining
// origins together in one batch.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateMultipleJoiningOriginsAllLessThanBatchSize) {
manager_->set_max_parallel_updates_for_testing(3);
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
constexpr char kGroupName1[] = "group1";
constexpr char kGroupName2[] = "group2";
constexpr char kGroupName3[] = "group3";
constexpr char kGroupName4[] = "group4";
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester_1 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_1 = rfh_tester_1->AppendChild("subframe1");
subframe_1 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_1);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.owner = kOriginC;
interest_group.update_url = kUrlC.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName1));
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupName2;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName2));
// Navigate to top frame kUrlC.
NavigateAndCommit(kUrlC);
content::RenderFrameHostTester* rfh_tester_2 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlC.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_2 = rfh_tester_2->AppendChild("subframe2");
subframe_2 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_2);
blink::InterestGroup interest_group_3 = CreateInterestGroup();
interest_group_3.name = kGroupName3;
interest_group_3.owner = kOriginC;
interest_group_3.update_url = kUrlC.Resolve(kUpdateUrlPath3);
interest_group_3.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_3.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_3.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_3, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName3));
blink::InterestGroup interest_group_4 = CreateInterestGroup();
interest_group_4.name = kGroupName4;
interest_group_4.owner = kOriginC;
interest_group_4.update_url = kUrlC.Resolve(kUpdateUrlPath4);
interest_group_4.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_4.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_4.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_4, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName3));
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath2);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath3);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath4);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 2u);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath, kServerResponse);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath2,
kServerResponse);
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 4u);
}
// If there are three groups with joining origin A, one group with joining
// origin C, and batch limitation size as three, it can send three groups with
// joining origin A in one batch.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateMultipleJoiningOriginsAndOneEqualToBatchSize) {
manager_->set_max_parallel_updates_for_testing(3);
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
constexpr char kGroupName1[] = "group1";
constexpr char kGroupName2[] = "group2";
constexpr char kGroupName3[] = "group3";
constexpr char kGroupName4[] = "group4";
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester_1 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_1 = rfh_tester_1->AppendChild("subframe1");
subframe_1 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_1);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.owner = kOriginC;
interest_group.update_url = kUrlC.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName1));
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupName2;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName2));
blink::InterestGroup interest_group_3 = CreateInterestGroup();
interest_group_3.name = kGroupName3;
interest_group_3.owner = kOriginC;
interest_group_3.update_url = kUrlC.Resolve(kUpdateUrlPath3);
interest_group_3.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_3.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_3.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_3, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName3));
// Navigate to top frame kUrlC.
NavigateAndCommit(kUrlC);
content::RenderFrameHostTester* rfh_tester_2 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlC.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_2 = rfh_tester_2->AppendChild("subframe2");
subframe_2 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_2);
blink::InterestGroup interest_group_4 = CreateInterestGroup();
interest_group_4.name = kGroupName4;
interest_group_4.owner = kOriginC;
interest_group_4.update_url = kUrlC.Resolve(kUpdateUrlPath4);
interest_group_4.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_4.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_4.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_4, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName4));
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath2);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath3);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath4);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 3u);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath, kServerResponse);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath2,
kServerResponse);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath3,
kServerResponse);
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 4u);
}
// If there are three groups with joining origin A, one group with joining
// origin C, and batch limitation size as two, it can send two groups with
// joining origin A in the first batch, and remain two groups with different
// joining origins together in the second batch.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateMultipleJoiningOriginsAndOneMoreThanBatchSize) {
manager_->set_max_parallel_updates_for_testing(2);
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
constexpr char kGroupName1[] = "group1";
constexpr char kGroupName2[] = "group2";
constexpr char kGroupName3[] = "group3";
constexpr char kGroupName4[] = "group4";
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester_1 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_1 = rfh_tester_1->AppendChild("subframe1");
subframe_1 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_1);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.owner = kOriginC;
interest_group.update_url = kUrlC.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName1));
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupName2;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName2));
blink::InterestGroup interest_group_3 = CreateInterestGroup();
interest_group_3.name = kGroupName3;
interest_group_3.owner = kOriginC;
interest_group_3.update_url = kUrlC.Resolve(kUpdateUrlPath3);
interest_group_3.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_3.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_3.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_3, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName3));
// Navigate to top frame kUrlC.
NavigateAndCommit(kUrlC);
content::RenderFrameHostTester* rfh_tester_2 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlC.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_2 = rfh_tester_2->AppendChild("subframe2");
subframe_2 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_2);
blink::InterestGroup interest_group_4 = CreateInterestGroup();
interest_group_4.name = kGroupName4;
interest_group_4.owner = kOriginC;
interest_group_4.update_url = kUrlC.Resolve(kUpdateUrlPath4);
interest_group_4.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_4.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_4.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_4, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName3));
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath2);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath3);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath4);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 2u);
if (network_responder_->HasPendingResponse(kUpdateUrlPath)) {
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath,
kServerResponse);
}
if (network_responder_->HasPendingResponse(kUpdateUrlPath2)) {
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath2,
kServerResponse);
}
if (network_responder_->HasPendingResponse(kUpdateUrlPath3)) {
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath3,
kServerResponse);
}
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 4u);
}
// Test if the interest group can still be able to update after delaying once.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateMultipleBatches) {
manager_->set_max_parallel_updates_for_testing(2);
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
constexpr char kGroupName1[] = "group1";
constexpr char kGroupName2[] = "group2";
constexpr char kGroupName3[] = "group3";
constexpr char kGroupName4[] = "group4";
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester_1 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_1 = rfh_tester_1->AppendChild("subframe1");
subframe_1 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_1);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.owner = kOriginC;
interest_group.update_url = kUrlC.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName1));
// Navigate to top frame kUrlC.
NavigateAndCommit(kUrlC);
content::RenderFrameHostTester* rfh_tester_2 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlC.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_2 = rfh_tester_2->AppendChild("subframe2");
subframe_2 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_2);
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupName2;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName2));
blink::InterestGroup interest_group_3 = CreateInterestGroup();
interest_group_3.name = kGroupName3;
interest_group_3.owner = kOriginC;
interest_group_3.update_url = kUrlC.Resolve(kUpdateUrlPath3);
interest_group_3.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_3.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_3.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_3, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName3));
// Navigate to top frame kUrlD.
NavigateAndCommit(kUrlD);
content::RenderFrameHostTester* rfh_tester_3 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame KUrlD.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_3 = rfh_tester_3->AppendChild("subframe3");
subframe_3 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_3);
blink::InterestGroup interest_group_4 = CreateInterestGroup();
interest_group_4.name = kGroupName4;
interest_group_4.owner = kOriginC;
interest_group_4.update_url = kUrlC.Resolve(kUpdateUrlPath4);
interest_group_4.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_4.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_4.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_4, subframe_3);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName4));
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath2);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath3);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath4);
UpdateInterestGroupNoFlushForFrame(subframe_3);
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath, kServerResponse);
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 3u);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath2,
kServerResponse);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath3,
kServerResponse);
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 4u);
}
// Join two interest groups with different joining origins and defer the update.
// Later, join another group with the same origin as the second one during the
// deferment. Verify that the second and third groups use different isolation
// information.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateIsolationMapIsClearedWithMixedJoiningOriginsAndNewJoinedGroup) {
manager_->set_max_parallel_updates_for_testing(2);
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
constexpr char kGroupName1[] = "group1";
constexpr char kGroupName2[] = "group2";
constexpr char kGroupName3[] = "group3";
net::IsolationInfo isolation_info2;
net::IsolationInfo isolation_info3;
base::RunLoop run_loop2;
base::RunLoop run_loop3;
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath2,
base::BindLambdaForTesting(
[&isolation_info2,
&run_loop2](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info2 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop2.Quit();
}));
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath3,
base::BindLambdaForTesting(
[&isolation_info3,
&run_loop3](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info3 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop3.Quit();
}));
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester_1 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_1 = rfh_tester_1->AppendChild("subframe1");
subframe_1 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_1);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.owner = kOriginC;
interest_group.update_url = kUrlC.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName1));
// Navigate to top frame kUrlC.
NavigateAndCommit(kUrlC);
content::RenderFrameHostTester* rfh_tester_2 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlC.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_2 = rfh_tester_2->AppendChild("subframe2");
subframe_2 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_2);
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupName2;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName2));
blink::InterestGroup interest_group_3 = CreateInterestGroup();
interest_group_3.name = kGroupName3;
interest_group_3.owner = kOriginC;
interest_group_3.update_url = kUrlC.Resolve(kUpdateUrlPath3);
interest_group_3.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_3.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_3.ads->emplace_back(std::move(ad));
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath2);
UpdateInterestGroupNoFlush();
// Fast forward a small amount of time to ensure `interest_group_3` joins
// after the update.
task_environment()->FastForwardBy(base::Seconds(1));
JoinInterestGroupAndFlush(interest_group_3, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName3));
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath, kServerResponse);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath2,
kServerResponse);
run_loop2.Run();
run_loop3.Run();
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 3u);
ASSERT_FALSE(isolation_info2.IsEqualForTesting(isolation_info3));
}
// Join an interest group with joining origin C update it, then join two more
// groups with joining origin A and C almost 24 hours later. Delay the update
// until the first group is ready for updating again. Confirm that the two
// C-joining_origin groups use different isolation info.
TEST_F(AdAuctionServiceImplDifferentNIKDuringUpdateTest,
UpdateIsolationMapIsClearedWithMixedJoiningOriginsAndNewValidGroup) {
manager_->set_max_parallel_updates_for_testing(2);
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
constexpr char kGroupName1[] = "group1";
constexpr char kGroupName2[] = "group2";
constexpr char kGroupName3[] = "group3";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kUpdateUrlPath2, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kUpdateUrlPath3, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
net::IsolationInfo isolation_info2;
net::IsolationInfo isolation_info3;
base::RunLoop run_loop2;
base::RunLoop run_loop3;
// Navigate to top frame kUrlC
NavigateAndCommit(kUrlC);
content::RenderFrameHostTester* rfh_tester_1 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlC.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_1 =
rfh_tester_1->AppendChild("subframe_1");
subframe_1 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_1);
blink::InterestGroup interest_group_3 = CreateInterestGroup();
interest_group_3.name = kGroupName3;
interest_group_3.owner = kOriginC;
interest_group_3.update_url = kUrlC.Resolve(kUpdateUrlPath3);
interest_group_3.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_3.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_3.ads->emplace_back(std::move(ad));
interest_group_3.expiry = base::Time::Now() + base::Days(3);
JoinInterestGroupAndFlush(interest_group_3, subframe_1);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName3));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// Fast forward time to 23 hours, 59 minutes and 59 seconds later.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod - base::Seconds(1));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// Register callback for group2 and group3 for `isolation_info` validation.
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath2,
base::BindLambdaForTesting(
[&isolation_info2,
&run_loop2](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info2 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop2.Quit();
}));
network_responder_->RegisterRepeatCallback(
kUpdateUrlPath3,
base::BindLambdaForTesting(
[&isolation_info3,
&run_loop3](URLLoaderInterceptor::RequestParams* params) {
if (params && params->url_request.trusted_params) {
isolation_info3 =
params->url_request.trusted_params->isolation_info;
} else {
ADD_FAILURE() << "No params or trusted_params";
}
run_loop3.Quit();
}));
// Navigate to top frame kUrlA.
NavigateAndCommit(kUrlA);
content::RenderFrameHostTester* rfh_tester_2 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlA.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_2 =
rfh_tester_2->AppendChild("subframe_2");
subframe_2 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_2);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.owner = kOriginC;
interest_group.update_url = kUrlC.Resolve(kUpdateUrlPath);
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe_2);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName1));
// Navigate to top frame kUrlC.
NavigateAndCommit(kUrlC);
content::RenderFrameHostTester* rfh_tester_3 =
content::RenderFrameHostTester::For(main_rfh());
// Attach a subframe with kUrlC to the top frame kUrlC.
// Create and update an interest group owned by kOriginC.
content::RenderFrameHost* subframe_3 =
rfh_tester_3->AppendChild("subframe_3");
subframe_3 =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe_3);
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kGroupName2;
interest_group_2.owner = kOriginC;
interest_group_2.update_url = kUrlC.Resolve(kUpdateUrlPath2);
interest_group_2.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2, subframe_3);
EXPECT_EQ(1, GetJoinCount(kOriginC, kGroupName2));
// Defer the update process for first batch.
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath2);
UpdateInterestGroupNoFlush();
// Fast forward time for 10 seconds to make group 3 become valid for update
// again.
task_environment()->FastForwardBy(base::Seconds(10));
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath, kServerResponse);
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath2,
kServerResponse);
run_loop2.Run();
run_loop3.Run();
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 4u);
ASSERT_FALSE(isolation_info2.IsEqualForTesting(isolation_info3));
}
// Join 2 interest groups, each with a different owner. When updating interest
// groups, only the 1 interest group owned by the origin of the frame that
// called navigator.updateAdInterestGroups() gets updated.
TEST_F(AdAuctionServiceImplTest, UpdateOnlyOwnOrigin) {
// Both interest groups can share the same update logic and path (they just
// use different origins).
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Now, join the second interest group, belonging to `kOriginB`.
NavigateAndCommit(kUrlB);
blink::InterestGroup interest_group_b = CreateInterestGroup();
interest_group_b.owner = kOriginB;
interest_group_b.update_url = kUrlB.Resolve(kUpdateUrlPath);
interest_group_b.bidding_url = kUrlB.Resolve(kBiddingUrlPath);
interest_group_b.trusted_bidding_signals_url =
kUrlB.Resolve(kTrustedBiddingSignalsUrlPath);
interest_group_b.trusted_bidding_signals_keys.emplace();
interest_group_b.trusted_bidding_signals_keys->push_back("key1");
interest_group_b.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_b.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_b);
EXPECT_EQ(1, GetJoinCount(kOriginB, kInterestGroupName));
// Now, run the update. Only the `kOriginB` group should get updated.
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The `kOriginB` interest group should update...
scoped_refptr<StorageInterestGroups> origin_b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(origin_b_groups->size(), 1u);
const auto& origin_b_group =
origin_b_groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(origin_b_group.name, kInterestGroupName);
ASSERT_TRUE(origin_b_group.ads.has_value());
ASSERT_EQ(origin_b_group.ads->size(), 1u);
EXPECT_EQ(origin_b_group.ads.value()[0].render_url(),
"https://example.com/new_render");
// ...but the `kOriginA` interest group shouldn't change.
scoped_refptr<StorageInterestGroups> origin_a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(origin_a_groups->size(), 1u);
const auto& origin_a_group =
origin_a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(origin_a_group.ads.has_value());
ASSERT_EQ(origin_a_group.ads->size(), 1u);
EXPECT_EQ(origin_a_group.ads.value()[0].render_url(),
"https://example.com/render");
}
// Test updating an interest group with a cross-site owner.
TEST_F(AdAuctionServiceImplTest, UpdateFromCrossSiteIFrame) {
// All interest groups can share the same update logic and path (they just
// use different origins).
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Now, join the second interest group, belonging to `kOriginB`.
NavigateAndCommit(kUrlB);
blink::InterestGroup interest_group_b = CreateInterestGroup();
interest_group_b.owner = kOriginB;
interest_group_b.update_url = kUrlB.Resolve(kUpdateUrlPath);
interest_group_b.bidding_url = kUrlB.Resolve(kBiddingUrlPath);
interest_group_b.trusted_bidding_signals_url =
kUrlB.Resolve(kTrustedBiddingSignalsUrlPath);
interest_group_b.trusted_bidding_signals_keys.emplace();
interest_group_b.trusted_bidding_signals_keys->push_back("key1");
interest_group_b.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_b.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_b);
EXPECT_EQ(1, GetJoinCount(kOriginB, kInterestGroupName));
// Now, join the third interest group, belonging to `kOriginC`.
NavigateAndCommit(kUrlC);
blink::InterestGroup interest_group_c = CreateInterestGroup();
interest_group_c.owner = kOriginC;
interest_group_c.update_url = kUrlC.Resolve(kUpdateUrlPath);
interest_group_c.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group_c.trusted_bidding_signals_url =
kUrlC.Resolve(kTrustedBiddingSignalsUrlPath);
interest_group_c.trusted_bidding_signals_keys.emplace();
interest_group_c.trusted_bidding_signals_keys->push_back("key1");
interest_group_c.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_c.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_c);
EXPECT_EQ(1, GetJoinCount(kOriginC, kInterestGroupName));
NavigateAndCommit(kUrlA);
// Create a subframe and use it to send the join request.
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
subframe =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe);
UpdateInterestGroupNoFlushForFrame(subframe);
task_environment()->RunUntilIdle();
// Subframes from origin C with a top frame of A should update groups
// with C as the owner, but the subframe from C should not be able to update
// groups for A.
// The `kOriginC` interest group should update...
scoped_refptr<StorageInterestGroups> origin_c_groups =
GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(origin_c_groups->size(), 1u);
const auto& origin_c_group =
origin_c_groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(origin_c_group.name, kInterestGroupName);
ASSERT_TRUE(origin_c_group.ads.has_value());
ASSERT_EQ(origin_c_group.ads->size(), 1u);
EXPECT_EQ(origin_c_group.ads.value()[0].render_url(),
"https://example.com/new_render");
// ...but the `kOriginA` interest group shouldn't change.
scoped_refptr<StorageInterestGroups> origin_a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(origin_a_groups->size(), 1u);
const auto& origin_a_group =
origin_a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(origin_a_group.ads.has_value());
ASSERT_EQ(origin_a_group.ads->size(), 1u);
EXPECT_EQ(origin_a_group.ads.value()[0].render_url(),
"https://example.com/render");
// Now try on disallowed subframe from originB.
subframe =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlB, subframe);
interest_group = CreateInterestGroup();
interest_group.owner = kOriginB;
UpdateInterestGroupNoFlushForFrame(subframe);
task_environment()->RunUntilIdle();
// Subframes from origin B with a top frame of A should not (by policy) be
// allowed to update groups with B as the owner.
scoped_refptr<StorageInterestGroups> origin_b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(origin_b_groups->size(), 1u);
const auto& origin_b_group =
origin_b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(origin_b_group.ads.has_value());
ASSERT_EQ(origin_b_group.ads->size(), 1u);
EXPECT_EQ(origin_b_group.ads.value()[0].render_url(),
"https://example.com/render");
}
// The `ads` field is valid, but one of its fields is invalid. The entire update
// should get cancelled, since updates are atomic.
TEST_F(AdAuctionServiceImplTest, UpdateInvalidFieldCancelsAllUpdates) {
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/"{\"ad\":\"metadata\",\"here\":[1,2,3]}");
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
struct TestCase {
const std::string render_url;
const std::string allowed_reporting_origins;
} kTestCases[] = {
{"https://invalid^&", ""},
{"http://test.com", ""},
{"https://test.com", R"(["http://example.com"])"},
{"https://test.com", R"(["https://1", "https://2","https://3","https://4",
"https://5","https://6","https://7","https://8","https://9","https://10","https://11"])"},
};
for (const auto& test_case : kTestCases) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath,
base::StringPrintf(R"({
"biddingLogicURL": "%s/interest_group/new_bidding_logic.js",
"ads": [{"renderURL": %s,
"metadata": {"new_a": "b"},
"allowedReportingOrigins": %s
}]
})",
kOriginStringA, test_case.render_url.c_str(),
test_case.allowed_reporting_origins.c_str()));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads and bidding logic URL didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
EXPECT_EQ(group.ads.value()[0].metadata,
"{\"ad\":\"metadata\",\"here\":[1,2,3]}");
EXPECT_EQ(group.bidding_url, kBiddingLogicUrlA);
}
}
// The `ads` field is valid, but one of its allowed reporting origins is not
// attested. The entire update should get cancelled.
TEST_F(AdAuctionServiceImplTest,
UpdateNotAttestedAllowedReportingOriginsCancelsAllUpdates) {
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/"{\"ad\":\"metadata\",\"here\":[1,2,3]}");
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
content_browser_client_.SetAllowList({kOriginG});
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"biddingLogicURL": "%s/interest_group/new_bidding_logic.js",
"ads": [{"renderURL": "https://test.com",
"metadata": {"new_a": "b"},
"allowedReportingOrigins": ["https://a.test", "https://g.test"]
}]
})",
kOriginStringA));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads and bidding logic URL didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
EXPECT_EQ(group.ads.value()[0].metadata,
"{\"ad\":\"metadata\",\"here\":[1,2,3]}");
EXPECT_FALSE(group.ads.value()[0].allowed_reporting_origins.has_value());
EXPECT_EQ(group.bidding_url, kBiddingLogicUrlA);
}
// The `priority` field is not a valid number. The entire update should get
// cancelled, since updates are atomic.
TEST_F(AdAuctionServiceImplTest, UpdateInvalidPriorityCancelsAllUpdates) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"priority": "high",
"biddingLogicURL": "%s/interest_group/new_bidding_logic.js"
})",
kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.priority = 2.0;
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/"{\"ad\":\"metadata\",\"here\":[1,2,3]}");
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the priority and bidding logic URL didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.priority, 2.0);
EXPECT_EQ(group.bidding_url, kBiddingLogicUrlA);
}
// The `sellerCapabilities` field has an invalid capability. The invalid
// capability gets skipped, but the rest of the update proceeds.
TEST_F(AdAuctionServiceImplTest, UpdateInvalidSellerCapabilitiesIgnored) {
// TODO(caraitto): Convert interestGroupCounts to interest-group-counts when
// support for the camelCase version is dropped.
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"sellerCapabilities": {"%s": ["latency-stats"], "*": ["interestGroupCounts",
"invalid-capability"]}
})",
kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the seller capabilities was updated, with the invalid capability
// ignored.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.all_sellers_capabilities,
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts}));
ASSERT_TRUE(group.seller_capabilities);
ASSERT_EQ(group.seller_capabilities->size(), 1u);
EXPECT_EQ(group.seller_capabilities->at(kOriginA),
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kLatencyStats}));
}
// The server response can't be parsed as valid JSON. The update is cancelled.
TEST_F(AdAuctionServiceImplTest, UpdateInvalidJSONIgnored) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath,
"This isn't JSON.");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
}
// UpdateJSONParserCrash fails on Android or with the Rust parser because in
// those conditions the data decoder doesn't use a separate process to parse
// JSON. On other platforms, the C++ parser runs out-of-proc for safety.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(BUILD_RUST_JSON_READER)
// The server response is valid, but we simulate the JSON parser (which may
// run in a separate process) crashing, so the update doesn't happen.
TEST_F(AdAuctionServiceImplTest, UpdateJSONParserCrash) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Simulate the JSON service crashing instead of returning a result.
data_decoder::test::InProcessDataDecoder in_process_data_decoder;
in_process_data_decoder.SimulateJsonParserCrash(
/*drop=*/true);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
auto group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// Try another IG update, this time with no crash. It should succceed.
// (We need to advance time since this next attempt is rate-limited).
in_process_data_decoder.SimulateJsonParserCrash(
/*drop=*/false);
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads *did* change this time.
groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
#endif // !BUILDFLAG(IS_ANDROID)
// Trigger an update, but block it via ContentBrowserClient policy.
// The update shouldn't happen.
TEST_F(AdAuctionServiceImplTest, UpdateBlockedByContentBrowserClient) {
NavigateAndCommit(kUrlNoUpdate);
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = kOriginNoUpdate;
interest_group.update_url = kUpdateUrlNoUpdate;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginNoUpdate, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginNoUpdate);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// There shouldn't have even been an attempt to update.
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
}
// The network request fails (not implemented), so the update is cancelled.
TEST_F(AdAuctionServiceImplTest, UpdateNetworkFailure) {
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUrlA.Resolve("no_handler.json");
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
}
// The network request for updating interest groups times out, so the update
// fails.
TEST_F(AdAuctionServiceImplTest, UpdateTimeout) {
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->FastForwardBy(base::Seconds(30) + base::Seconds(1));
task_environment()->RunUntilIdle();
// The request times out (ERR_TIMED_OUT), so the ads should not change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
}
// Start an update, and delay the server response so that the interest group
// expires before the interest group updates. Don't advance time enough for DB
// maintenance tasks to run -- that is the interest group will only exist on
// disk in an expired state, and not appear in queries.
TEST_F(AdAuctionServiceImplTest,
UpdateDuringInterestGroupExpirationNoDbMaintenence) {
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
// Make the interest group expire before the DB maintenance task should be
// run, with a gap second where expiration has happened, but DB maintenance
// has not. Time order:
// (*NOW*, group expiration, db maintenance).
const base::TimeDelta kExpiryDelta =
InterestGroupStorage::kIdlePeriod - base::Seconds(2);
ASSERT_GT(kExpiryDelta, base::Seconds(0));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + kExpiryDelta;
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Start an interest group update and then advance time to ensure the interest
// group expires before a response is returned.
UpdateInterestGroupNoFlush();
task_environment()->FastForwardBy(kExpiryDelta + base::Seconds(1));
task_environment()->RunUntilIdle();
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
EXPECT_EQ(0u, GetInterestGroupsForOwner(kOriginA)->size());
// Due to FastForwardBy(), we're at this time order:
// (group expiration, *NOW*, db maintenance).
// So, DB maintenance should not have been run.
base::RunLoop run_loop;
manager_->GetLastMaintenanceTimeForTesting(
base::BindLambdaForTesting([&run_loop](base::Time time) {
EXPECT_EQ(time, base::Time::Min());
run_loop.Quit();
}));
run_loop.Run();
// Now return the server response. The interest group shouldn't change as it's
// expired.
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath, kServerResponse);
task_environment()->RunUntilIdle();
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
EXPECT_EQ(0u, GetInterestGroupsForOwner(kOriginA)->size());
// Updating again when the interest group has been deleted shouldn't somehow
// bring it back -- also, advance past the rate limit window to ensure the
// update actually happens.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod + base::Seconds(1));
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, kServerResponse);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
EXPECT_EQ(0u, GetInterestGroupsForOwner(kOriginA)->size());
// DB maintenance never occurs since we never FastForward() past db
// maintenance. We still are at time order:
// (group expiration, *NOW*, db maintenance).
}
// Start an update, and delay the server response so that the interest group
// expires before the interest group updates. Advance time enough for DB
// maintenance tasks to run -- that is the interest group will be deleted from
// the database.
TEST_F(AdAuctionServiceImplTest,
UpdateDuringInterestGroupExpirationWithDbMaintenence) {
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
// Make the interest group expire just before the DB maintenance task should
// be run. Time order:
// (*NOW*, group expiration, db maintenance).
const base::Time now = base::Time::Now();
const base::TimeDelta kExpiryDelta =
InterestGroupStorage::kIdlePeriod - base::Seconds(1);
ASSERT_GT(kExpiryDelta, base::Seconds(0));
const base::Time next_maintenance_time =
now + InterestGroupStorage::kIdlePeriod;
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = now + kExpiryDelta;
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Start an interest group update and then advance time to ensure the interest
// group expires and then DB maintenance is performed, both before a response
// is returned.
UpdateInterestGroupNoFlush();
task_environment()->FastForwardBy(InterestGroupStorage::kIdlePeriod +
base::Seconds(1));
task_environment()->RunUntilIdle();
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
EXPECT_EQ(0u, GetInterestGroupsForOwner(kOriginA)->size());
// Due to FastForwardBy(), we're at this time order:
// (group expiration, db maintenance, *NOW*).
// So, DB maintenance should have been run.
base::RunLoop run_loop;
manager_->GetLastMaintenanceTimeForTesting(base::BindLambdaForTesting(
[&run_loop, next_maintenance_time](base::Time time) {
EXPECT_EQ(time, next_maintenance_time);
run_loop.Quit();
}));
run_loop.Run();
// Now return the server response. The interest group shouldn't change as it's
// expired.
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath, kServerResponse);
task_environment()->RunUntilIdle();
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
EXPECT_EQ(0u, GetInterestGroupsForOwner(kOriginA)->size());
// Updating again when the interest group has been deleted shouldn't somehow
// bring it back -- also, advance past the rate limit window to ensure the
// update actually happens.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod + base::Seconds(1));
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, kServerResponse);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
EXPECT_EQ(0u, GetInterestGroupsForOwner(kOriginA)->size());
}
// Start an update, and delay the server response so that the test ends before
// the interest group finishes updating. Nothing should crash.
TEST_F(AdAuctionServiceImplTest, UpdateNeverFinishesBeforeDestruction) {
// We never respond to this request.
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Start an interest group update, but never respond to network requests. The
// update shouldn't happen.
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// No updates have happened yet, nor will they before the test ends.
scoped_refptr<StorageInterestGroups> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
const auto& a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(), "https://example.com/render");
// The test ends while the update is in progress. Nothing should crash as we
// run destructors.
}
// The update doesn't happen because the update URL isn't specified at
// Join() time.
TEST_F(AdAuctionServiceImplTest, DoesntChangeGroupsWithNoUpdateUrl) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads didn't change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
}
// Register a bid and a win, then perform a successful update. The bid and win
// stats shouldn't change.
TEST_F(AdAuctionServiceImplTest, UpdateDoesntChangeBrowserSignals) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroupKey originA_group_key(kOriginA, kInterestGroupName);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Register 2 bids and a win.
manager_->RecordInterestGroupBids(blink::InterestGroupSet{originA_group_key});
manager_->RecordInterestGroupBids(blink::InterestGroupSet{originA_group_key});
manager_->RecordInterestGroupWin(originA_group_key, "{}");
scoped_refptr<StorageInterestGroups> prev_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(prev_groups->size(), 1u);
const auto& prev_signals =
prev_groups->GetInterestGroups()[0]->bidding_browser_signals;
EXPECT_EQ(prev_signals->join_count, 1);
EXPECT_EQ(prev_signals->bid_count, 2);
EXPECT_EQ(prev_signals->prev_wins.size(), 1u);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The group updates, but the signals don't.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
const auto& signals = groups->GetInterestGroups()[0]->bidding_browser_signals;
EXPECT_EQ(signals->join_count, 1);
EXPECT_EQ(signals->bid_count, 2);
EXPECT_EQ(signals->prev_wins.size(), 1u);
EXPECT_EQ(group.name, kInterestGroupName);
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Join an interest group.
// Update interest group successfully.
// Change update response to different value.
// Update attempt does nothing (rate limited).
// Advance to just before time limit drops, update does nothing (rate limited).
// Advance after time limit. Update should work.
TEST_F(AdAuctionServiceImplTest, UpdateRateLimitedAfterSuccessfulUpdate) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The first update completes successfully.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
// Change the update response and try updating again.
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
scoped_refptr<StorageInterestGroups> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2->size(), 1u);
const auto& group2 = groups2->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group2.ads.value()[0].render_url(),
"https://example.com/new_render");
// Advance time to just before end of rate limit period. Update should still
// do nothing due to rate limiting.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod - base::Seconds(1));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
scoped_refptr<StorageInterestGroups> groups3 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups3->size(), 1u);
const auto& group3 = groups3->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group3.ads.has_value());
ASSERT_EQ(group3.ads->size(), 1u);
EXPECT_EQ(group3.ads.value()[0].render_url(),
"https://example.com/new_render");
// Advance time to just after end of rate limit period. Update should now
// succeed.
task_environment()->FastForwardBy(base::Seconds(2));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update changes the database contents.
scoped_refptr<StorageInterestGroups> groups4 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups4->size(), 1u);
const auto& group4 = groups4->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group4.ads.has_value());
ASSERT_EQ(group4.ads->size(), 1u);
EXPECT_EQ(group4.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Join an interest group.
// Set up update to fail (return invalid server response).
// Update interest group fails.
// Change update response to different value that will succeed.
// Update does nothing (rate limited).
// Advance to just before rate limit drops (which for bad response is the longer
// "successful" duration), update does nothing (rate limited).
// Advance after time limit. Update should work.
TEST_F(AdAuctionServiceImplTest, UpdateRateLimitedAfterBadUpdateResponse) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath,
"This isn't JSON.");
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The first update fails, nothing changes.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// Change the update response and try updating again.
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
scoped_refptr<StorageInterestGroups> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2->size(), 1u);
const auto& group2 = groups2->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// Advance time to just before end of rate limit period. Update should still
// do nothing due to rate limiting. Invalid responses use the longer
// "successful" backoff period.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod - base::Seconds(1));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
scoped_refptr<StorageInterestGroups> groups3 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups3->size(), 1u);
const auto& group3 = groups3->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group3.ads.has_value());
ASSERT_EQ(group3.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// Advance time to just after end of rate limit period. Update should now
// succeed.
task_environment()->FastForwardBy(base::Seconds(2));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update changes the database contents.
scoped_refptr<StorageInterestGroups> groups4 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups4->size(), 1u);
const auto& group4 = groups4->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group4.ads.has_value());
ASSERT_EQ(group4.ads->size(), 1u);
EXPECT_EQ(group4.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Just like AdAuctionServiceImplTest.UpdateRateLimitedAfterBadUpdateResponse,
// except server returns a valid JSON response for update but with un-enrolled
// allowedReportingOrigins.
//
// Join an interest group.
// Set up update to fail (return server response with un-enrolled origins).
// Update interest group fails.
// Change update response to different value that will succeed.
// Update does nothing (rate limited).
// Advance to just before rate limit drops (which for bad response is the longer
// "successful" duration), update does nothing (rate limited).
// Advance after time limit. Update should work.
TEST_F(AdAuctionServiceImplTest,
UpdateRateLimitedAfterGotNotAttestedAllowedReportingOrigins) {
content_browser_client_.SetAllowList({kOriginB});
network_responder_->RegisterUpdateResponse(kUpdateUrlPath,
R"({
"ads": [{"renderURL": "https://example.com/new_render",
"allowedReportingOrigins": ["https://a.test"]
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The first update fails, nothing changes.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
EXPECT_FALSE(group.ads.value()[0].allowed_reporting_origins.has_value());
// Change the allowedReportingOrigins to attested origins and try updating
// again.
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render",
"allowedReportingOrigins": ["https://b.test"]
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
scoped_refptr<StorageInterestGroups> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2->size(), 1u);
const auto& group2 = groups2->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
EXPECT_FALSE(group.ads.value()[0].allowed_reporting_origins.has_value());
// Advance time to just before end of rate limit period. Update should still
// do nothing due to rate limiting. Invalid responses use the longer
// "successful" backoff period.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateSucceededBackoffPeriod - base::Seconds(1));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
scoped_refptr<StorageInterestGroups> groups3 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups3->size(), 1u);
const auto& group3 = groups3->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group3.ads.has_value());
ASSERT_EQ(group3.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
EXPECT_FALSE(group.ads.value()[0].allowed_reporting_origins.has_value());
// Advance time to just after end of rate limit period. Update should now
// succeed.
task_environment()->FastForwardBy(base::Seconds(2));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update changes the database contents.
scoped_refptr<StorageInterestGroups> groups4 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups4->size(), 1u);
const auto& group4 = groups4->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group4.ads.has_value());
ASSERT_EQ(group4.ads->size(), 1u);
EXPECT_EQ(group4.ads.value()[0].render_url(),
"https://example.com/new_render");
std::vector<url::Origin> allowed_reporting_origins = {kOriginB};
EXPECT_EQ(group4.ads.value()[0].allowed_reporting_origins,
allowed_reporting_origins);
}
// Join an interest group.
// Make interest group update fail with net::ERR_CONNECTION_RESET.
// Update interest group fails.
// Change update response to succeed.
// Update does nothing (rate limited).
// Advance to just before rate limit drops, update does nothing (rate limited).
// Advance after time limit. Update should work.
TEST_F(AdAuctionServiceImplTest, UpdateRateLimitedAfterFailedUpdate) {
network_responder_->FailNextUpdateRequestWithError(net::ERR_CONNECTION_RESET);
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The first update fails, nothing changes.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// Change the update response and try updating again.
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
scoped_refptr<StorageInterestGroups> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2->size(), 1u);
const auto& group2 = groups2->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// Advance time to just before end of rate limit period. Update should still
// do nothing due to rate limiting.
task_environment()->FastForwardBy(
InterestGroupStorage::kUpdateFailedBackoffPeriod - base::Seconds(1));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
scoped_refptr<StorageInterestGroups> groups3 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups3->size(), 1u);
const auto& group3 = groups3->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group3.ads.has_value());
ASSERT_EQ(group3.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// Advance time to just after end of rate limit period. Update should now
// succeed.
task_environment()->FastForwardBy(base::Seconds(2));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update changes the database contents.
scoped_refptr<StorageInterestGroups> groups4 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups4->size(), 1u);
const auto& group4 = groups4->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group4.ads.has_value());
ASSERT_EQ(group4.ads->size(), 1u);
EXPECT_EQ(group4.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// net::ERR_INTERNET_DISCONNECTED skips rate limiting, unlike other errors.
//
// Join an interest group.
// Make interest group update fail with net::ERR_INTERNET_DISCONNECTED.
// Update interest group fails.
// Change update response to different value that will succeed.
// Update succeeds (not rate limited).
TEST_F(AdAuctionServiceImplTest, UpdateNotRateLimitedIfDisconnected) {
network_responder_->FailNextUpdateRequestWithError(
net::ERR_INTERNET_DISCONNECTED);
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The first update fails (internet disconnected), nothing changes.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
// Change the update response and try updating again.
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update changes the database contents -- no rate limiting occurs.
scoped_refptr<StorageInterestGroups> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2->size(), 1u);
const auto& group2 = groups2->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group2.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Nothing crashes if we have a disconnect and a successful update in-flight at
// the same time.
//
// Join 2 interest groups that have the same owner.
//
// Update both interest groups; the first has a delayed response, and the second
// fails with net::ERR_INTERNET_DISCONNECTED. After that, the first update
// response arrives.
//
// Check that the second interest group is not updated. Intentionally don't
// whether the first interest group updates or not.
//
// Nothing should crash.
//
// Afterwards, updating should successfully update both interest groups, without
// rate limiting.
TEST_F(AdAuctionServiceImplTest, DisconnectedAndSuccessInFlightTogether) {
// Create 2 interest groups belonging to the same owner.
const std::string kServerResponse1 = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
blink::InterestGroup interest_group_1 = CreateInterestGroup();
interest_group_1.expiry = base::Time::Now() + base::Days(30);
interest_group_1.update_url = kUpdateUrlA;
interest_group_1.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_1.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_1);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
network_responder_->FailUpdateRequestWithError(
kUpdateUrlPath2, net::ERR_INTERNET_DISCONNECTED);
task_environment()->FastForwardBy(base::Seconds(1));
constexpr char kInterestGroupName2[] = "group2";
blink::InterestGroup interest_group_2 = CreateInterestGroup();
interest_group_2.name = kInterestGroupName2;
interest_group_2.expiry = base::Time::Now() + base::Days(30);
interest_group_2.update_url = kUpdateUrlA2;
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_2.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_2);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName2));
// Start the update. The second group update will fail with
// ERR_INTERNET_DISCONNECTED.
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Now, let the first group's update response be sent.
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath,
kServerResponse1);
task_environment()->RunUntilIdle();
// The second update fails (internet disconnected), so that interest group
// doesn't update. We don't have any particular requirement what happens to
// the "successful" update that happened at the same time.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 2u);
auto group_2 = groups->GetInterestGroups()[0]->interest_group;
ASSERT_EQ(group_2.name, kInterestGroupName2);
ASSERT_TRUE(group_2.ads.has_value());
ASSERT_EQ(group_2.ads->size(), 1u);
EXPECT_EQ(group_2.ads.value()[0].render_url(), "https://example.com/render");
// Now, try to update both interest groups. Both should now succeed.
const std::string kServerResponse2 = R"({
"ads": [{"renderURL": "https://example.com/new_render2"}]
})";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, kServerResponse1);
network_responder_->RegisterUpdateResponse(kUpdateUrlPath2, kServerResponse2);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that both groups updated.
groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 2u);
std::vector<SingleStorageInterestGroup> single_groups =
groups->GetInterestGroups();
group_2 = single_groups[0]->interest_group;
auto group_1 = single_groups[1]->interest_group;
ASSERT_EQ(group_1.name, kInterestGroupName);
ASSERT_EQ(group_2.name, kInterestGroupName2);
ASSERT_TRUE(group_1.ads.has_value());
ASSERT_EQ(group_1.ads->size(), 1u);
EXPECT_EQ(group_1.ads.value()[0].render_url(),
"https://example.com/new_render");
ASSERT_TRUE(group_2.ads.has_value());
ASSERT_EQ(group_2.ads->size(), 1u);
EXPECT_EQ(group_2.ads.value()[0].render_url(),
"https://example.com/new_render2");
}
// Fire off many updates rapidly in a loop. Only one update should happen.
TEST_F(AdAuctionServiceImplTest, UpdateRateLimitedTightLoop) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
for (size_t i = 0; i < 1000u; i++) {
UpdateInterestGroupNoFlush();
}
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// One of the updates completes successfully.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Test that creates 3 interest groups for different origins, then runs update
// for each origin, with the first update delayed.
//
// The second and third IGs shouldn't get updated until the first is allowed to
// proceed.
TEST_F(AdAuctionServiceImplTest, OnlyOneOriginUpdatesAtATime) {
// kOriginA's update will be deferred, whereas kOriginB's and kOriginC's
// updates will be allowed to proceed immediately.
constexpr char kServerResponseA[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterUpdateResponse(kUpdateUrlPathB, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kUpdateUrlPathC, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
// Create interest group for kOriginA.
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Create interest group for kOriginB.
NavigateAndCommit(kUrlB);
interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.owner = kOriginB;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlB;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginB, kInterestGroupName));
// Create interest group for kOriginC.
NavigateAndCommit(kUrlC);
interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to the next rate limit
// period without the interest group expiring.
interest_group.owner = kOriginC;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlC;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginC, kInterestGroupName));
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
// Attempt to update kOriginA's interest groups. The update doesn't happen
// yet, because the server delays its response.
NavigateAndCommit(kUrlA);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(), "https://example.com/render");
// Now, try to update kOriginB's interest groups. The update shouldn't happen
// yet, because we're still updating kOriginA's interest groups.
NavigateAndCommit(kUrlB);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
auto b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render");
// Now, try to update kOriginC's interest groups. The update shouldn't happen
// yet, because we're still updating kOriginA's interest groups.
NavigateAndCommit(kUrlC);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> c_groups =
GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(c_groups->size(), 1u);
auto c_group = c_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(c_group.ads.has_value());
ASSERT_EQ(c_group.ads->size(), 1u);
EXPECT_EQ(c_group.ads.value()[0].render_url(), "https://example.com/render");
// Only one network request should have been made (for the kOriginA update).
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// Now, the server finishes sending the kOriginA response. Both interest
// groups should now update, since kOriginA's update completion unblocks
// kOriginB's update.
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath,
kServerResponseA);
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 3u);
// kOriginA's groups have updated.
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
// kOriginB's groups have updated.
b_groups = GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(),
"https://example.com/new_render");
// kOriginC's groups have updated.
b_groups = GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(b_groups->size(), 1u);
b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Set the maximum number of parallel updates to 2. Create three interest
// groups, each in origin A, and update origin A's interest groups.
//
// Check that all the interest groups updated.
TEST_F(AdAuctionServiceImplTest, UpdatesInBatches) {
manager_->set_max_parallel_updates_for_testing(2);
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
// Create 3 interest groups for kOriginA.
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
constexpr char kInterestGroupName2[] = "group2";
interest_group = CreateInterestGroup();
interest_group.name = kInterestGroupName2;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName2));
constexpr char kInterestGroupName3[] = "group3";
interest_group = CreateInterestGroup();
interest_group.name = kInterestGroupName3;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName3));
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
// Update all interest groups.
NavigateAndCommit(kUrlA);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 3u);
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 3u);
for (const SingleStorageInterestGroup& group : groups->GetInterestGroups()) {
ASSERT_TRUE(group->interest_group.ads.has_value());
ASSERT_EQ(group->interest_group.ads->size(), 1u);
EXPECT_EQ(group->interest_group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
}
// Set the maximum number of parallel updates to 2. Create three interest
// groups, each in origin A, and update origin A's interest groups. Make one
// fail, and one timeout.
//
// Check that the interest group that didn't fail or timeout updates
// successfully.
TEST_F(AdAuctionServiceImplTest, UpdatesInBatchesWithFailuresAndTimeouts) {
manager_->set_max_parallel_updates_for_testing(2);
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->FailUpdateRequestWithError(kUpdateUrlPath2,
net::ERR_CONNECTION_RESET);
// We never respond to this -- just let it timeout.
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath3);
// Create 3 interest groups for kOriginA -- give them different update URLs to
// so that some timeout and some fail.
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
constexpr char kInterestGroupName2[] = "group2";
interest_group = CreateInterestGroup();
interest_group.name = kInterestGroupName2;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA2;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName2));
constexpr char kInterestGroupName3[] = "group3";
interest_group = CreateInterestGroup();
interest_group.name = kInterestGroupName3;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA3;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName3));
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
// Update all interest groups.
NavigateAndCommit(kUrlA);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Requests are issued in random order. If the first or second request is the
// delayed request, the third request won't be issued, since the first 2
// aren't complete. On the other hand, if the delayed request is the third
// request, all three update requests would have been issued by now.
EXPECT_GE(network_responder_->UpdateCount(), 2u);
EXPECT_LE(network_responder_->UpdateCount(), 3u);
// Now, fast forward so that the hanging request times out. After this, all
// updates should be completed.
task_environment()->FastForwardBy(base::Seconds(31));
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 3u);
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 3u);
for (const auto& group : groups->GetInterestGroups()) {
const auto& ads = group->interest_group.ads;
ASSERT_TRUE(ads.has_value());
ASSERT_EQ(ads->size(), 1u);
if (group->interest_group.update_url == kUpdateUrlA) {
EXPECT_EQ(ads.value()[0].render_url(), "https://example.com/new_render");
} else {
EXPECT_EQ(ads.value()[0].render_url(), "https://example.com/render");
}
}
}
// Create an interest group in a.test, and in b.test. Defer the update response
// for a.test, and update a.test and b.test.
//
// Wait the max update round duration, then respond to the a.test update
// request. The a.test interest group should update, but the b.test update
// should be cancelled.
//
// Then, try updating b.test normally, without deferral. The update should
// complete successfully.
TEST_F(AdAuctionServiceImplTest, CancelsLongstandingUpdates) {
// Lower the max update round duration so that it is smaller than the network
// timeout.
//
// The production value is much longer than the interest group
// network timeout, so to exceed the production max update round duration,
// we'd need to do delayed updates for a large number of interest groups. The
// test override avoids this awkwardness while still exercising the same
// scenario.
constexpr base::TimeDelta kMaxUpdateRoundDuration = base::Seconds(5);
manager_->set_max_update_round_duration_for_testing(kMaxUpdateRoundDuration);
// kOriginA's update will be deferred, whereas kOriginB's
// update will be allowed to proceed immediately.
constexpr char kServerResponseA[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterUpdateResponse(kUpdateUrlPathB, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
// Create interest group for kOriginA.
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to update cancellation
// without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Create interest group for kOriginB.
NavigateAndCommit(kUrlB);
interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to update cancellation
// without the interest group expiring.
interest_group.owner = kOriginB;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlB;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginB, kInterestGroupName));
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
// Attempt to update kOriginA's interest groups. The update doesn't happen
// yet, because the server delays its response.
NavigateAndCommit(kUrlA);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(), "https://example.com/render");
// Now, try to update kOriginB's interest groups. The update shouldn't happen
// yet, because we're still updating kOriginA's interest groups.
NavigateAndCommit(kUrlB);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
auto b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render");
// Only one network request should have been made (for the kOriginA update).
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// Advance time beyond the max update round duration. This will result in
// kOriginB's update getting cancelled, but kOriginA's update will still be
// able to proceed because it's in-progress.
task_environment()->FastForwardBy(kMaxUpdateRoundDuration + base::Seconds(1));
// Now, the server finishes sending the kOriginA response. Both interest
// groups should now update, since kOriginA's update completion unblocks
// kOriginB's update. However, kOriginB's update never happens, because it
// gets cancelled.
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath,
kServerResponseA);
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// kOriginA's groups have updated.
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
// But kOriginB's groups have not updated, because they got cancelled.
b_groups = GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render");
// Now, try updating kOriginB. The update should complete successfully.
network_responder_->RegisterUpdateResponse(kUpdateUrlPathB, R"({
"ads": [{"renderURL": "https://example.com/newer_render"
}]
})");
NavigateAndCommit(kUrlB);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// kOriginB's groups have updated.
b_groups = GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(),
"https://example.com/newer_render");
}
// Like CancelsLongstandingUpdates, but after the cancellation, tries to update
// a different origin, c.test, that succeeds.
//
// NOTE that a.test won't qualify for update until the next day due to rate
// limiting, since it successfully updated.
TEST_F(AdAuctionServiceImplTest, CancelsLongstandingUpdates2) {
// Lower the max update round duration so that it is smaller than the network
// timeout.
//
// The production value is much longer than the interest group
// network timeout, so to exceed the production max update round duration,
// we'd need to do delayed updates for a large number of interest groups. The
// test override avoids this awkwardness while still exercising the same
// scenario.
constexpr base::TimeDelta kMaxUpdateRoundDuration = base::Seconds(5);
manager_->set_max_update_round_duration_for_testing(kMaxUpdateRoundDuration);
// kOriginA's update will be deferred, whereas kOriginB's
// update will be allowed to proceed immediately.
constexpr char kServerResponseA[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"}]
})";
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath);
network_responder_->RegisterUpdateResponse(kUpdateUrlPathB, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
// Create interest group for kOriginA.
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to update cancellation
// without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Create interest group for kOriginB.
NavigateAndCommit(kUrlB);
interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to update cancellation
// without the interest group expiring.
interest_group.owner = kOriginB;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlB;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginB, kInterestGroupName));
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
// Attempt to update kOriginA's interest groups. The update doesn't happen
// yet, because the server delays its response.
NavigateAndCommit(kUrlA);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(), "https://example.com/render");
// Now, try to update kOriginB's interest groups. The update shouldn't happen
// yet, because we're still updating kOriginA's interest groups.
NavigateAndCommit(kUrlB);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
auto b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render");
// Only one network request should have been made (for the kOriginA update).
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// Advance time beyond the max update round duration. This will result in
// kOriginB's update getting cancelled, but kOriginA's update will still be
// able to proceed because it's in-progress.
task_environment()->FastForwardBy(kMaxUpdateRoundDuration + base::Seconds(1));
// Now, the server finishes sending the kOriginA response. Both interest
// groups should now update, since kOriginA's update completion unblocks
// kOriginB's update. However, kOriginB's update never happens, because it
// gets cancelled.
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath,
kServerResponseA);
task_environment()->RunUntilIdle();
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// kOriginA's groups have updated.
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
// But kOriginB's groups have not updated, because they got cancelled.
b_groups = GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render");
// Now, try updating a new origin, kOriginC. The update should complete
// successfully.
// Create interest group for kOriginC.
NavigateAndCommit(kUrlC);
interest_group = CreateInterestGroup();
interest_group.owner = kOriginC;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlC;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
ASSERT_TRUE(interest_group.IsValid());
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginC, kInterestGroupName));
network_responder_->RegisterUpdateResponse(kUpdateUrlPathC, R"({
"ads": [{"renderURL": "https://example.com/newer_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// kOriginC's groups have updated.
auto c_groups = GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(c_groups->size(), 1u);
auto c_group = c_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(c_group.ads.has_value());
ASSERT_EQ(c_group.ads->size(), 1u);
EXPECT_EQ(c_group.ads.value()[0].render_url(),
"https://example.com/newer_render");
// But kOriginB's groups have not updated.
b_groups = GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render");
}
// After a round of updates completes, the round cancellation timer should reset
// so that future updates can proceed.
//
// Create 2 interest groups in different origins. Update the first, then wait
// for more than the max update round duration, then update the second.
//
// Both interest groups should update correctly.
TEST_F(AdAuctionServiceImplTest, UpdateCancellationTimerClearedOnCompletion) {
// Set the max update duration to a known value.
constexpr base::TimeDelta kMaxUpdateRoundDuration = base::Seconds(5);
manager_->set_max_update_round_duration_for_testing(kMaxUpdateRoundDuration);
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kUpdateUrlPathB, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
// Create interest group for kOriginA.
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to update cancellation
// without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Create interest group for kOriginB.
NavigateAndCommit(kUrlB);
interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to update cancellation
// without the interest group expiring.
interest_group.owner = kOriginB;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlB;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginB, kInterestGroupName));
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
// Attempt to update kOriginA's interest groups. The update completes
// successfully.
NavigateAndCommit(kUrlA);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
// Only one network request should have been made (for the kOriginA update).
EXPECT_EQ(network_responder_->UpdateCount(), 1u);
// Advance time beyond the max update round duration.
task_environment()->FastForwardBy(kMaxUpdateRoundDuration + base::Seconds(1));
// Now, try to update kOriginB's interest groups. The update completes
// successfully.
NavigateAndCommit(kUrlB);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
auto b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(),
"https://example.com/new_render");
// Two network requests should have been made (for the kOriginA and kOriginB
// updates).
EXPECT_EQ(network_responder_->UpdateCount(), 2u);
}
// Create 4 interest groups in a.test, and one in b.test.
//
// For the a.test groups, have one succeed immediately, one fail immediately
// (invalid JSON), one be delayed a duration shorter than the max update round
// duration (and succeed), and one delayed a duration more than the max update
// round duration (and succeed).
//
// For the b.test group, let it succeed immediately.
//
// Update all groups, advancing time twice to issue the 2 a.test delayed
// responses.
//
// All a.test updates except the failed update should succeed. The b.test update
// should be cancelled.
//
// Then, try updating b.test normally, without deferral. The update should
// complete successfully.
TEST_F(AdAuctionServiceImplTest, CancelsLongstandingUpdatesComplex) {
// Lower the max update round duration so that it is smaller than the network
// timeout.
//
// The production value is much longer than the interest group
// network timeout, so to exceed the production max update round duration,
// we'd need to do delayed updates for a large number of interest groups. The
// test override avoids this awkwardness while still exercising the same
// scenario.
constexpr base::TimeDelta kMaxUpdateRoundDuration = base::Seconds(5);
manager_->set_max_update_round_duration_for_testing(kMaxUpdateRoundDuration);
// 2 of kOriginA's updates will be deferred (each by different amounts of
// time) and one will be allowed to proceed immediately, whereas kOriginB's 1
// update will be allowed to proceed immediately. The last group's update will
// fail.
constexpr char kServerResponse[] = R"({
"ads": [{"renderURL": "https://example.com/render2"}]
})";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, kServerResponse);
network_responder_->FailUpdateRequestWithError(kUpdateUrlPath2,
net::ERR_CONNECTION_RESET);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath3);
network_responder_->RegisterDeferredUpdateResponse(kUpdateUrlPath4);
network_responder_->RegisterUpdateResponse(kUpdateUrlPathB, kServerResponse);
// Create interest groups for kOriginA.
for (const GURL& update_url :
{kUpdateUrlA, kUpdateUrlA2, kUpdateUrlA3, kUpdateUrlA4}) {
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to update cancellation
// without the interest group expiring.
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.name = update_url.path();
interest_group.update_url = update_url;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, /*name=*/update_url.path()));
}
// Create interest group for kOriginB.
NavigateAndCommit(kUrlB);
blink::InterestGroup interest_group = CreateInterestGroup();
// Set a long expiration delta so that we can advance to update cancellation
// without the interest group expiring.
interest_group.owner = kOriginB;
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlB;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginB, kInterestGroupName));
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
// Attempt to update kOriginA's interest groups. The first 2 interest group
// updates complete (success and failure). The remaining updates don't
// happen yet, because the server delays its response.
NavigateAndCommit(kUrlA);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 4u);
bool seen_succeeded = false, seen_failed = false;
for (const SingleStorageInterestGroup& a_group :
a_groups->GetInterestGroups()) {
const blink::InterestGroup& group = a_group->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
if (group.name == kUpdateUrlA.path()) {
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/render2");
seen_succeeded = true;
continue;
} else if (group.name == kUpdateUrlA2.path()) {
seen_failed = true;
}
// Failed and deferred interest groups shouldn't have updated.
EXPECT_EQ(group.ads.value()[0].render_url(), "https://example.com/render");
}
EXPECT_TRUE(seen_succeeded);
EXPECT_TRUE(seen_failed);
// Now, try to update kOriginB's interest groups. The update shouldn't happen
// yet, because we're still updating kOriginA's interest groups.
NavigateAndCommit(kUrlB);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
auto b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render");
// Only 4 network requests should have been made (for the kOriginA updates).
EXPECT_EQ(network_responder_->UpdateCount(), 4u);
// Advance time to just before the max update round duration, then issue the
// server response for one of the interest group updates. It should update
// immediately.
task_environment()->FastForwardBy(kMaxUpdateRoundDuration - base::Seconds(1));
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath3,
kServerResponse);
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 4u);
for (const SingleStorageInterestGroup& a_group :
a_groups->GetInterestGroups()) {
const blink::InterestGroup& group = a_group->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
if (group.name == kUpdateUrlA3.path()) {
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/render2");
break;
}
}
EXPECT_EQ(network_responder_->UpdateCount(), 4u);
// Advance time beyond the max update round duration. This will result in
// kOriginB's update getting cancelled, but kOriginA's last update will still
// be able to proceed because it's in-progress.
task_environment()->FastForwardBy(base::Seconds(2));
// Now, the server finishes sending the last kOriginA response. Both it and
// kOriginB's interest groups should now update, since the completion of
// kOriginA's last update unblocks kOriginB's update. However, kOriginB's
// update never happens, because it gets cancelled.
network_responder_->DoDeferredUpdateResponse(kUpdateUrlPath4,
kServerResponse);
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 4u);
for (const SingleStorageInterestGroup& a_group :
a_groups->GetInterestGroups()) {
const blink::InterestGroup& group = a_group->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
if (group.name == kUpdateUrlA4.path()) {
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/render2");
break;
}
}
EXPECT_EQ(network_responder_->UpdateCount(), 4u);
// kOriginB's group hasn't been updated, because the update got cancelled.
b_groups = GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render");
// Now, try updating kOriginB. The update should complete successfully.
network_responder_->RegisterUpdateResponse(kUpdateUrlPathB, R"({
"ads": [{"renderURL": "https://example.com/render3"
}]
})");
NavigateAndCommit(kUrlB);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// kOriginB's groups have updated.
b_groups = GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups->size(), 1u);
b_group = b_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(b_group.ads.has_value());
ASSERT_EQ(b_group.ads->size(), 1u);
EXPECT_EQ(b_group.ads.value()[0].render_url(), "https://example.com/render3");
}
TEST_F(AdAuctionServiceImplTest, CreateAuctionNonce) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeatures(
{blink::features::kFledgeNegativeTargeting},
{blink::features::kFledgeCreateAuctionNonceSynchronousResolution});
mojo::Remote<blink::mojom::AdAuctionService> ad_auction_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), ad_auction_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
base::Uuid auction_nonce;
ad_auction_service->CreateAuctionNonce(base::BindLambdaForTesting(
[&run_loop, &auction_nonce](const base::Uuid& nonce) {
auction_nonce = nonce;
run_loop.Quit();
}));
run_loop.Run();
EXPECT_NE(auction_nonce.AsLowercaseString(), "");
}
// Calling CreateAuctionNonce() with `kFledgeNegativeTargeting` feature
// disabled should not be possible.
TEST_F(AdAuctionServiceImplTest,
CreateAuctionNonceDisabledBecauseNegativeTargetingDisabled) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeatures(
{}, {blink::features::kFledgeNegativeTargeting,
blink::features::kFledgeCreateAuctionNonceSynchronousResolution});
mojo::Remote<blink::mojom::AdAuctionService> ad_auction_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), ad_auction_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
ad_auction_service.set_disconnect_handler(run_loop.QuitClosure());
ad_auction_service->CreateAuctionNonce(
base::BindOnce([](const base::Uuid& nonce) {
ADD_FAILURE() << "Callback unexpectedly invoked.";
}));
run_loop.Run();
}
// CreateAuctionNonce() should not be called when the
// `FledgeCreateAuctionNonceSynchronousResolution` feature is enabled.
TEST_F(AdAuctionServiceImplTest,
CreateAuctionNonceDisabledBecauseOfSynchronousResolution) {
base::test::ScopedFeatureList feature_list;
feature_list.InitWithFeatures(
{blink::features::kFledgeNegativeTargeting,
blink::features::kFledgeCreateAuctionNonceSynchronousResolution},
{});
mojo::Remote<blink::mojom::AdAuctionService> ad_auction_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), ad_auction_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
ad_auction_service.set_disconnect_handler(run_loop.QuitClosure());
ad_auction_service->CreateAuctionNonce(
base::BindOnce([](const base::Uuid& nonce) {
ADD_FAILURE() << "Callback unexpectedly invoked.";
}));
run_loop.Run();
}
// Passing in a config with `auction_nonce` set while
// `kFledgeNegativeTargeting` feature is disabled should not be possible.
TEST_F(AdAuctionServiceImplTest, RunAdAuctionNonceDisabled) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(blink::features::kFledgeNegativeTargeting);
mojo::Remote<blink::mojom::AdAuctionService> ad_auction_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), ad_auction_service.BindNewPipeAndPassReceiver());
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.non_shared_params.auction_nonce =
base::Uuid::GenerateRandomV4();
base::RunLoop run_loop;
ad_auction_service.set_disconnect_handler(run_loop.QuitClosure());
ad_auction_service->RunAdAuction(
auction_config, mojo::NullReceiver(),
base::BindOnce(
[](bool aborted_by_script,
const std::optional<blink::FencedFrame::RedactedFencedFrameConfig>&
config) {
ADD_FAILURE() << "Callback unexpectedly invoked.";
}));
ad_auction_service.FlushForTesting();
run_loop.Run();
}
// Passing in a config with `auction_nonce` set while
// `kFledgeNegativeTargeting` feature is disabled should not be possible.
TEST_F(AdAuctionServiceImplTest, RunAdAuctionNonceOnComponentDisabled) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(blink::features::kFledgeNegativeTargeting);
mojo::Remote<blink::mojom::AdAuctionService> ad_auction_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), ad_auction_service.BindNewPipeAndPassReceiver());
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.non_shared_params.auction_nonce =
base::Uuid::GenerateRandomV4();
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
base::RunLoop run_loop;
ad_auction_service.set_disconnect_handler(run_loop.QuitClosure());
ad_auction_service->RunAdAuction(
auction_config, mojo::NullReceiver(),
base::BindOnce(
[](bool aborted_by_script,
const std::optional<blink::FencedFrame::RedactedFencedFrameConfig>&
config) {
ADD_FAILURE() << "Callback unexpectedly invoked.";
}));
ad_auction_service.FlushForTesting();
run_loop.Run();
}
// Add an interest group, and run an ad auction.
TEST_F(AdAuctionServiceImplTest, RunAdAuction) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render"));
// Running the auction alone should not result in updating the interest
// group's bid count or previous win list, no matter how much time passes.
task_environment()->RunUntilIdle();
std::optional<SingleStorageInterestGroup> storage_interest_group(
GetInterestGroup(interest_group.owner, interest_group.name));
ASSERT_TRUE(storage_interest_group.has_value());
EXPECT_EQ(0,
storage_interest_group.value()->bidding_browser_signals->bid_count);
EXPECT_EQ(0u, storage_interest_group.value()
->bidding_browser_signals->prev_wins.size());
// Invoking the URN callback (which is done when the result is loaded in a
// frame) updates those fields.
InvokeCallbackForURN(*auction_result);
std::optional<SingleStorageInterestGroup>
storage_interest_group_after_callback(
GetInterestGroup(interest_group.owner, interest_group.name));
ASSERT_TRUE(storage_interest_group_after_callback.has_value());
EXPECT_EQ(1, storage_interest_group_after_callback.value()
->bidding_browser_signals->bid_count);
ASSERT_EQ(1u, storage_interest_group_after_callback.value()
->bidding_browser_signals->prev_wins.size());
ASSERT_EQ(R"({"renderURL":"https://example.com/render"})",
storage_interest_group_after_callback.value()
->bidding_browser_signals->prev_wins[0]
->ad_json);
// The auction should also trigger a k-anon "join" for the winning ad.
EXPECT_THAT(GetKAnonJoinedIds(),
::testing::UnorderedElementsAre(
KAnonKeyForAdBid(interest_group,
interest_group.ads.value()[0].render_url()),
KAnonKeyForAdNameReporting(interest_group,
interest_group.ads.value()[0])));
}
// Add an interest group, and run an ad auction. Seller rejects the bid. Bid
// count should be updated.
TEST_F(AdAuctionServiceImplTest, RunAdAuctionSellerRejectsBid) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return -1;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
// The bid count should be updated immediately, since theere's no URN to wait
// to be loaded in a frame.
std::optional<SingleStorageInterestGroup> storage_interest_group(
GetInterestGroup(interest_group.owner, interest_group.name));
ASSERT_TRUE(storage_interest_group.has_value());
EXPECT_EQ(1,
storage_interest_group.value()->bidding_browser_signals->bid_count);
EXPECT_EQ(0u, storage_interest_group.value()
->bidding_browser_signals->prev_wins.size());
// The auction should not trigger any k-anon "joins".
EXPECT_THAT(GetKAnonJoinedIds(), ::testing::UnorderedElementsAre());
}
// Run ad auction when number of urn mappings has reached limit, the action
// should fail.
TEST_F(AdAuctionServiceImplTest,
RunAdAuctionExceedNumOfUrnMappingsLimitFailsAuction) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
FencedFrameURLMapping& fenced_frame_urls_map =
static_cast<RenderFrameHostImpl*>(main_rfh())
->GetPage()
.fenced_frame_urls_map();
FencedFrameURLMappingTestPeer fenced_frame_url_mapping_test_peer(
&fenced_frame_urls_map);
// Fill the map until its size reaches the limit.
GURL url("https://a.test");
fenced_frame_url_mapping_test_peer.FillMap(url);
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
// Auction failed because the number of urn mappings has reached limit.
ASSERT_EQ(auction_result, std::nullopt);
}
// Runs an auction, and expects that the interest group that participated in
// the auction gets updated after the auction completes.
//
// Create an interest group. Run an auction with that interest group.
//
// The interest group should be updated after the auction completes.
TEST_F(AdAuctionServiceImplTest, UpdatesInterestGroupsAfterSuccessfulAuction) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
content_browser_client_.SetAllowList({kOriginG, kOriginF});
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render",
"allowedReportingOrigins":
["https://g.test", "https://f.test", "https://g.test"]
}]})");
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render"));
// Now that the auction has completed, check that the interest group updated.
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
ASSERT_TRUE(a_group.ads.value()[0].allowed_reporting_origins.has_value());
EXPECT_THAT(a_group.ads.value()[0].allowed_reporting_origins.value(),
::testing::UnorderedElementsAre(kOriginF, kOriginG));
}
// Like UpdatesInterestGroupsAfterSuccessfulAuction but
// kFledgeDelayPostAuctionInterestGroupUpdate is enabled.
TEST_F(AdAuctionServiceImplTest,
UpdatesInterestGroupsAfterSuccessfulAuctionDelayedUpdate) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
features::kFledgeDelayPostAuctionInterestGroupUpdate);
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
content_browser_client_.SetAllowList({kOriginG, kOriginF});
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render",
"allowedReportingOrigins":
["https://g.test", "https://f.test", "https://g.test"]
}]})");
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render"));
// Now that the auction has completed, check that the interest group is
// updated after kPostAuctionInterestGroupUpdateDelay (but not before).
task_environment()->FastForwardBy(base::Seconds(1));
// The update shouldn't have happened yet.
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(a_group.ads, interest_group_a.ads);
task_environment()->FastForwardBy(
AuctionRunner::kPostAuctionInterestGroupUpdateDelay);
// Now the update should have happened.
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
ASSERT_TRUE(a_group.ads.value()[0].allowed_reporting_origins.has_value());
EXPECT_THAT(a_group.ads.value()[0].allowed_reporting_origins.value(),
::testing::UnorderedElementsAre(kOriginF, kOriginG));
}
// Like UpdatesInterestGroupsAfterSuccessfulAuction, but the auction fails
// because the scoring script always returns 0. The interest group should still
// update.
TEST_F(AdAuctionServiceImplTest, UpdatesInterestGroupsAfterFailedAuction) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return 0;
}
)";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
// Now that the auction has completed, check that the interest group updated.
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Like UpdatesInterestGroupsAfterFailedAuction, but the auction fails because
// the decision script can't be loaded. The interest group still updates.
TEST_F(AdAuctionServiceImplTest,
UpdatesInterestGroupsAfterFailedAuctionMissingScript) {
constexpr char kMissingScriptPath[] = "/script-not-found.js";
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->FailRequestWithError(kMissingScriptPath,
net::ERR_FILE_NOT_FOUND);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kMissingScriptPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
// Now that the auction has completed, check that the interest group updated.
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Trigger a post auction update, but block it via ContentBrowserClient policy.
// The update shouldn't happen.
TEST_F(AdAuctionServiceImplTest,
UpdatesInterestGroupsAfterAuctionBlockedByContentBrowserClient) {
NavigateAndCommit(kUrlNoUpdate);
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_no_update = CreateInterestGroup();
interest_group_no_update.owner = kOriginNoUpdate;
interest_group_no_update.update_url = kUpdateUrlNoUpdate;
interest_group_no_update.bidding_url = kUrlNoUpdate.Resolve(kBiddingUrlPath);
interest_group_no_update.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_no_update.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_no_update);
EXPECT_EQ(1, GetJoinCount(kOriginNoUpdate, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginNoUpdate;
auction_config.decision_logic_url = kUrlNoUpdate.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginNoUpdate};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render"));
// Now that the auction has completed, check that the interest group didn't
// update.
task_environment()->RunUntilIdle();
auto no_update_groups = GetInterestGroupsForOwner(kOriginNoUpdate);
ASSERT_EQ(no_update_groups->size(), 1u);
auto no_update_group =
no_update_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(no_update_group.ads.has_value());
ASSERT_EQ(no_update_group.ads->size(), 1u);
EXPECT_EQ(no_update_group.ads.value()[0].render_url(),
"https://example.com/render");
// There shouldn't have even been an attempt to update.
EXPECT_EQ(network_responder_->UpdateCount(), 0u);
}
// Like UpdatesInterestGroupsAfterAuction, but with a component auction.
//
// Create 2 interest groups, each in different origins, A and C (we can't use B
// because AllowInterestGroupContentBrowserClient doesn't allow B interest
// groups to participate in A auctions). Run a component auction where A is a
// buyer in one component auction, and C is a buyer in another component
// auction. A wins.
//
// Both interest groups should be updated after the auction completes.
TEST_F(AdAuctionServiceImplTest,
UpdatesInterestGroupsAfterComponentAuctionWithWinner) {
constexpr char kBiddingScript1[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render1',
'allowComponentAuction': true};
}
)";
constexpr char kBiddingScript2[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 2, 'render': 'https://example.com/render2',
'allowComponentAuction': true};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return {desirability: bid, allowComponentAuction: true};
}
)";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kUpdateUrlPathC, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript1);
network_responder_->RegisterScriptResponse(kNewBiddingUrlPath,
kBiddingScript2);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render1"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
NavigateAndCommit(kUrlC);
blink::InterestGroup interest_group_b = CreateInterestGroup();
interest_group_b.owner = kOriginC;
interest_group_b.update_url = kUpdateUrlC;
interest_group_b.bidding_url = kUrlC.Resolve(kNewBiddingUrlPath);
interest_group_b.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render2"),
/*metadata=*/std::nullopt);
interest_group_b.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_b);
EXPECT_EQ(1, GetJoinCount(kOriginC, kInterestGroupName));
NavigateAndCommit(kUrlA);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
blink::AuctionConfig component_auction2;
component_auction2.seller = kOriginA;
component_auction2.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction2.non_shared_params.interest_group_buyers = {kOriginC};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction2));
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render2"));
// Now that the auction has completed, check that the interest groups updated.
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
auto c_groups = GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(c_groups->size(), 1u);
auto c_group = c_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(c_group.ads.has_value());
ASSERT_EQ(c_group.ads->size(), 1u);
EXPECT_EQ(c_group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Like UpdatesInterestGroupsAfterComponentAuctionWithWinner, but there's no
// winner, since the decision script scores every bid as 0.
//
// All participating interest groups should still be updated.
TEST_F(AdAuctionServiceImplTest,
UpdatesInterestGroupsAfterComponentAuctionWithNoWinner) {
constexpr char kBiddingScript1[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 2, 'render': 'https://example.com/render1',
'allowComponentAuction': true};
}
)";
constexpr char kBiddingScript2[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render2',
'allowComponentAuction': true};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return {desirability: 0, allowComponentAuction: true};
}
)";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kUpdateUrlPathC, R"({
"ads": [{"renderURL": "https://example.com/new_render"
}]
})");
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript1);
network_responder_->RegisterScriptResponse(kNewBiddingUrlPath,
kBiddingScript2);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render1"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
NavigateAndCommit(kUrlC);
blink::InterestGroup interest_group_b = CreateInterestGroup();
interest_group_b.owner = kOriginC;
interest_group_b.update_url = kUpdateUrlC;
interest_group_b.bidding_url = kUrlC.Resolve(kNewBiddingUrlPath);
interest_group_b.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render2"),
/*metadata=*/std::nullopt);
interest_group_b.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_b);
EXPECT_EQ(1, GetJoinCount(kOriginC, kInterestGroupName));
NavigateAndCommit(kUrlA);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
blink::AuctionConfig component_auction2;
component_auction2.seller = kOriginA;
component_auction2.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction2.non_shared_params.interest_group_buyers = {kOriginC};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction2));
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
// Now that the auction has completed, check that the interest groups updated.
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render");
auto c_groups = GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(c_groups->size(), 1u);
auto c_group = c_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(c_group.ads.has_value());
ASSERT_EQ(c_group.ads->size(), 1u);
EXPECT_EQ(c_group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
// Like UpdatesInterestGroupsAfterSuccessfulAuction, but neither the interest
// group nor the update have any ads.
TEST_F(AdAuctionServiceImplTest, UpdatesInterestGroupsAfterAuctionNoAds) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"trustedBiddingSignalsURL":
"%s/interest_group/new_trusted_bidding_signals_url.json",
"trustedBiddingSignalsKeys": ["new_key"]
})",
kOriginStringA));
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
// Now that the auction has completed, check that the interest group updated.
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.trusted_bidding_signals_url.has_value());
EXPECT_EQ(a_group.trusted_bidding_signals_url->spec(),
base::StringPrintf(
"%s/interest_group/new_trusted_bidding_signals_url.json",
kOriginStringA));
ASSERT_TRUE(a_group.trusted_bidding_signals_keys.has_value());
EXPECT_EQ(a_group.trusted_bidding_signals_keys->size(), 1u);
EXPECT_EQ(a_group.trusted_bidding_signals_keys.value()[0], "new_key");
EXPECT_FALSE(a_group.ads.has_value());
}
TEST_F(AdAuctionServiceImplTest, UpdateSupportsDeprecatedNames) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render"
}],
"sellerCapabilities": {
"*": ["interestGroupCounts", "latencyStats"]
},
"executionMode": "groupByOrigin"
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
EXPECT_EQ(group.all_sellers_capabilities,
blink::SellerCapabilitiesType(
{blink::SellerCapabilities::kInterestGroupCounts,
blink::SellerCapabilities::kLatencyStats}));
EXPECT_EQ(group.execution_mode,
blink::InterestGroup::ExecutionMode::kGroupedByOriginMode);
}
TEST_F(AdAuctionServiceImplTest, UpdateIgnoresUnknownEnumFields) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render"
}],
"sellerCapabilities": {
"https://example.test": ["non-valid-capability"]
},
"executionMode": "non-valid-execution-mode"
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
// The unknown enum values are ignored, and renderURL is updated.
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
}
TEST_F(AdAuctionServiceImplTest, UpdateRenamedFields) {
blink::InterestGroup initial_interest_group = CreateInterestGroup();
initial_interest_group.update_url = kUpdateUrlA;
JoinInterestGroupAndFlush(initial_interest_group);
// Update priority (which is *not* being renamed) in addition to target field
// to update -- this way, if the update fails, the test can observe that
// priority wasn't updated.
blink::InterestGroup updated_interest_group_ads = CreateInterestGroup();
updated_interest_group_ads.update_url = kUpdateUrlA;
updated_interest_group_ads.priority = 1.0;
updated_interest_group_ads.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
updated_interest_group_ads.ads->emplace_back(std::move(ad));
blink::InterestGroup updated_interest_group_ad_components =
CreateInterestGroup();
updated_interest_group_ad_components.update_url = kUpdateUrlA;
updated_interest_group_ad_components.priority = 1.0;
updated_interest_group_ad_components.ad_components.emplace();
blink::InterestGroup::Ad ad_component(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
updated_interest_group_ad_components.ad_components->emplace_back(
std::move(ad_component));
blink::InterestGroup updated_interest_group_bidding_logic_url =
CreateInterestGroup();
updated_interest_group_bidding_logic_url.update_url = kUpdateUrlA;
updated_interest_group_bidding_logic_url.priority = 1.0;
updated_interest_group_bidding_logic_url.bidding_url =
GURL(base::StringPrintf("%s/bidding.js", kOriginStringA));
blink::InterestGroup updated_interest_group_bidding_wasm_helper_url =
CreateInterestGroup();
updated_interest_group_bidding_wasm_helper_url.update_url = kUpdateUrlA;
updated_interest_group_bidding_wasm_helper_url.priority = 1.0;
updated_interest_group_bidding_wasm_helper_url.bidding_wasm_helper_url =
GURL(base::StringPrintf("%s/bidding.wasm", kOriginStringA));
blink::InterestGroup updated_interest_group_trusted_bidding_signals_url =
CreateInterestGroup();
updated_interest_group_trusted_bidding_signals_url.update_url = kUpdateUrlA;
updated_interest_group_trusted_bidding_signals_url.priority = 1.0;
updated_interest_group_trusted_bidding_signals_url
.trusted_bidding_signals_url =
GURL(base::StringPrintf("%s/signals.json", kOriginStringA));
struct TestCase {
const std::string update_contents;
const raw_ref<const blink::InterestGroup> expected_group;
} kTestCases[] = {
// ***
// ads renderURL
// ***
{R"("ads": [{"renderUrl": "https://example.com/render"}])",
raw_ref(updated_interest_group_ads)},
{R"("ads": [{"renderUrl": "https://example.com/render",
"renderURL": "https://example.com/render"}])",
raw_ref(updated_interest_group_ads)},
{R"("ads": [{"renderUrl": "https://example.com/render",
"renderURL": "https://example.com/render2"}])",
raw_ref(initial_interest_group)},
{R"("ads": [{}])", raw_ref(initial_interest_group)},
// ***
// adComponents renderURL
// ***
{R"("adComponents": [{"renderUrl": "https://example.com/render"}])",
raw_ref(updated_interest_group_ad_components)},
{R"("adComponents": [{"renderUrl": "https://example.com/render",
"renderURL": "https://example.com/render"}])",
raw_ref(updated_interest_group_ad_components)},
{R"("adComponents": [{"renderUrl": "https://example.com/render",
"renderURL": "https://example.com/render2"}])",
raw_ref(initial_interest_group)},
{R"("adComponents": [{}])", raw_ref(initial_interest_group)},
// ***
// biddingLogicURL
// ***
{base::StringPrintf(R"("biddingLogicUrl": "%s/bidding.js")",
kOriginStringA),
raw_ref(updated_interest_group_bidding_logic_url)},
{base::StringPrintf(R"("biddingLogicURL": "%s/bidding.js")",
kOriginStringA),
raw_ref(updated_interest_group_bidding_logic_url)},
{base::StringPrintf(R"("biddingLogicUrl": "%s/bidding.js",)"
R"("biddingLogicURL": "%s/bidding.js")",
kOriginStringA, kOriginStringA),
raw_ref(updated_interest_group_bidding_logic_url)},
{base::StringPrintf(R"("biddingLogicUrl": "%s/bidding.js",)"
R"("biddingLogicURL": "%s/bidding2.js")",
kOriginStringA, kOriginStringA),
raw_ref(initial_interest_group)},
// ***
// biddingWasmHelperURL
// ***
{base::StringPrintf(R"("biddingWasmHelperUrl": "%s/bidding.wasm")",
kOriginStringA),
raw_ref(updated_interest_group_bidding_wasm_helper_url)},
{base::StringPrintf(R"("biddingWasmHelperURL": "%s/bidding.wasm")",
kOriginStringA),
raw_ref(updated_interest_group_bidding_wasm_helper_url)},
{base::StringPrintf(R"("biddingWasmHelperUrl": "%s/bidding.wasm",)"
R"("biddingWasmHelperURL": "%s/bidding.wasm")",
kOriginStringA, kOriginStringA),
raw_ref(updated_interest_group_bidding_wasm_helper_url)},
{base::StringPrintf(R"("biddingWasmHelperUrl": "%s/bidding.wasm",)"
R"("biddingWasmHelperURL": "%s/bidding2.wasm")",
kOriginStringA, kOriginStringA),
raw_ref(initial_interest_group)},
// ***
// trustedBiddingSignalsURL
// ***
{base::StringPrintf(R"("trustedBiddingSignalsUrl": "%s/signals.json")",
kOriginStringA),
raw_ref(updated_interest_group_trusted_bidding_signals_url)},
{base::StringPrintf(R"("trustedBiddingSignalsURL": "%s/signals.json")",
kOriginStringA),
raw_ref(updated_interest_group_trusted_bidding_signals_url)},
{base::StringPrintf(R"("trustedBiddingSignalsUrl": "%s/signals.json",)"
R"("trustedBiddingSignalsURL": "%s/signals.json")",
kOriginStringA, kOriginStringA),
raw_ref(updated_interest_group_trusted_bidding_signals_url)},
{base::StringPrintf(R"("trustedBiddingSignalsUrl": "%s/signals.json",)"
R"("trustedBiddingSignalsURL": "%s/signals2.json")",
kOriginStringA, kOriginStringA),
raw_ref(initial_interest_group)},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.update_contents);
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({
"priority": 1.0,
%s
})",
test_case.update_contents.c_str()));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
EXPECT_TRUE(
groups->GetInterestGroups()[0]->interest_group.IsEqualForTesting(
*test_case.expected_group));
// Reset for the next iteration.
JoinInterestGroupAndFlush(initial_interest_group);
}
}
class AdAuctionServiceImplUpdateIfOlderThanTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplUpdateIfOlderThanTest() {
feature_list_.InitAndEnableFeature(
features::kInterestGroupUpdateIfOlderThan);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Join and manually update an interest group so that it's not eligible to
// update again for the successful update period. Advance a small amount of
// time. Then, run an auction with trusted bidding signals that specify an
// updateIfOlderThanMs greater than the time advanced, but less than the
// successful update period. The group should update successfully. Then, try
// updating again without advancing time -- the update should fail.
TEST_F(AdAuctionServiceImplUpdateIfOlderThanTest, OlderThan) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render0"
}]})");
// 3600000 ms == 1 hour.
network_responder_->RegisterSignalsResponse(
kTrustedBiddingSignalsUrlPath,
base::BindLambdaForTesting(
[](URLLoaderInterceptor::RequestParams* params) {
constexpr char kResponse[] = R"(
{
"keys": {
"key1": 1
},
"perInterestGroupData": {
"interest-group-name": {
"updateIfOlderThanMs": 3600000
}
}
}
)";
URLLoaderInterceptor::WriteResponse(
base::StrCat(
{kFledgeSignalsHeaders,
"Ad-Auction-Bidding-Signals-Format-Version: 2\n"}),
kResponse, params->client.get());
}));
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.expiry = base::Time::Now() + base::Days(10);
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.trusted_bidding_signals_url =
kUrlA.Resolve(kTrustedBiddingSignalsUrlPath);
interest_group_a.trusted_bidding_signals_keys = {"key1"};
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_gurl=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Update the interest group -- it should succeed.
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render0");
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render1"
}]})");
constexpr base::TimeDelta kFastForwardDelta(base::Hours(2));
ASSERT_GT(InterestGroupStorage::kUpdateSucceededBackoffPeriod,
kFastForwardDelta);
task_environment()->FastForwardBy(kFastForwardDelta);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/new_render0"));
// Now that the auction has completed, check that the interest group
// updated again.
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render1");
// Try to update again without advancing time. The update should be
// rate-limited, so the interest group shouldn't change.
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render2"
}]})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render1");
}
// Like AdAuctionServiceImplUpdateIfOlderThanTest.OlderThan, but we don't
// advance time, so the updateIfOlderThanMs directive takes no effect (since
// the group has been updated too recently -- it's not "older than"), so the
// post-auction update doesn't happen.
TEST_F(AdAuctionServiceImplUpdateIfOlderThanTest, NotOlderThan) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render0"
}]})");
// 3600000 ms == 1 hour.
network_responder_->RegisterSignalsResponse(
kTrustedBiddingSignalsUrlPath,
base::BindLambdaForTesting(
[](URLLoaderInterceptor::RequestParams* params) {
constexpr char kResponse[] = R"(
{
"keys": {
"key1": 1
},
"perInterestGroupData": {
"interest-group-name": {
"updateIfOlderThanMs": 3600000
}
}
}
)";
URLLoaderInterceptor::WriteResponse(
base::StrCat(
{kFledgeSignalsHeaders,
"Ad-Auction-Bidding-Signals-Format-Version: 2\n"}),
kResponse, params->client.get());
}));
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.expiry = base::Time::Now() + base::Days(10);
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.trusted_bidding_signals_url =
kUrlA.Resolve(kTrustedBiddingSignalsUrlPath);
interest_group_a.trusted_bidding_signals_keys = {"key1"};
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_gurl=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Update the interest group -- it should succeed.
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render0");
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render1"
}]})");
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/new_render0"));
// Now that the auction has completed, check if the interest group
// updated again -- it shouldn't have, because we didn't advance time.
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render0");
// Try to explicitly update again without advancing time. The update should be
// rate-limited, so the interest group shouldn't change.
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render2"
}]})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render0");
}
// Like AdAuctionServiceImplUpdateIfOlderThanTest.OlderThan, but with a
// updateIfOlderThanMs that's under 10 minutes. This time will be clamped to 10
// minutes.
TEST_F(AdAuctionServiceImplUpdateIfOlderThanTest, Clamped10Min) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
const ad = interestGroup.ads[0];
return {'ad': ad, 'bid': 1, 'render': ad.renderURL};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render0"
}]})");
// 30000 ms == 5 minutes.
network_responder_->RegisterSignalsResponse(
kTrustedBiddingSignalsUrlPath,
base::BindLambdaForTesting(
[](URLLoaderInterceptor::RequestParams* params) {
constexpr char kResponse[] = R"(
{
"keys": {
"key1": 1
},
"perInterestGroupData": {
"interest-group-name": {
"updateIfOlderThanMs": 30000
}
}
}
)";
URLLoaderInterceptor::WriteResponse(
base::StrCat(
{kFledgeSignalsHeaders,
"Ad-Auction-Bidding-Signals-Format-Version: 2\n"}),
kResponse, params->client.get());
}));
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group_a = CreateInterestGroup();
interest_group_a.expiry = base::Time::Now() + base::Days(10);
interest_group_a.update_url = kUpdateUrlA;
interest_group_a.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group_a.trusted_bidding_signals_url =
kUrlA.Resolve(kTrustedBiddingSignalsUrlPath);
interest_group_a.trusted_bidding_signals_keys = {"key1"};
interest_group_a.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_gurl=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group_a.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_a);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Update the interest group -- it should succeed.
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
auto a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render0");
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render1"
}]})");
// Fast forward 9 minutes. This is more than the requested 5 minutes, but
// updateIfOlderThanMs time older than 10 minutes get clamped to 10 minutes,
// so the next update still won't happen.
constexpr base::TimeDelta kFastForwardDelta(base::Minutes(9));
ASSERT_GT(InterestGroupStorage::kUpdateSucceededBackoffPeriod,
kFastForwardDelta);
task_environment()->FastForwardBy(kFastForwardDelta);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/new_render0"));
// Now that the auction has completed, check if the interest group
// updated again -- it shouldn't have.
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render0");
// Now, advance another minute. The interest group should now be 10 minutes
// old, so updateIfOlderThanMs should trigger an update.
constexpr base::TimeDelta kFastForwardExtraDelta(base::Minutes(9));
ASSERT_GT(InterestGroupStorage::kUpdateSucceededBackoffPeriod,
kFastForwardDelta + kFastForwardExtraDelta);
task_environment()->FastForwardBy(kFastForwardExtraDelta);
auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/new_render0"));
// Now that the auction has completed, check if the interest group updated
// again -- this time it finally should have.
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render1");
// Try to update again without advancing time. The update should be
// rate-limited, so the interest group shouldn't change.
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"ads": [{
"renderURL": "https://example.com/new_render2"
}]})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups->size(), 1u);
a_group = a_groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(a_group.ads.has_value());
ASSERT_EQ(a_group.ads->size(), 1u);
EXPECT_EQ(a_group.ads.value()[0].render_url(),
"https://example.com/new_render1");
}
// When sending reports, the next report request is feteched after the previous
// report request completed (`max_active_report_requests_` is set to 1 in this
// test). Reporting should continue even after the page navigated away. Timeout
// works for report requests.
TEST_F(AdAuctionServiceImplTest, SendReports) {
manager_->set_reporting_interval_for_testing(base::Seconds(5));
manager_->set_max_active_report_requests_for_testing(1);
network_responder_->RegisterScriptResponse(kBiddingUrlPath,
BasicBiddingReportScript());
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
network_responder_->RegisterReportResponse("/report_bidder", /*response=*/"");
network_responder_->RegisterStoreUrlLoaderClient("/report_seller");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
task_environment()->FastForwardBy(base::Seconds(30) - base::Seconds(1));
// There should only be the seller report, and the bidder report request is
// not fetched because the seller report request hangs and didn't finish yet.
EXPECT_EQ(network_responder_->ReportCount(), 1u);
// The request to seller's report url should hang before 30s.
EXPECT_TRUE(network_responder_->RemoteIsConnected());
task_environment()->FastForwardBy(base::Seconds(2));
// The request to seller report url should be disconnected after 30s due to
// timeout.
EXPECT_FALSE(network_responder_->RemoteIsConnected());
// Reporting should continue even after the page navigated away.
NavigateAndCommit(kUrlB);
// Navigating away normally deletes the AdAuctionServiceImpl. With this test
// fixture, however, the frame doesn't own the service so have to delete it
// manually.
DestroyAdAuctionService();
task_environment()->FastForwardBy(base::Seconds(1));
// The next request will not be sent when it's less than 5 seconds after the
// previous request completed.
EXPECT_EQ(network_responder_->ReportCount(), 1u);
task_environment()->FastForwardBy(base::Seconds(4));
// There should be two reports in total now, since the seller's report request
// completed (timed out) and then the bidder's report request was also fetched
// after 5 seconds since then.
EXPECT_EQ(network_responder_->ReportCount(), 2u);
}
// Check that reports aren't sent until the URN to URL callback is invoked.
TEST_F(AdAuctionServiceImplTest, SendReportsWaitsForCallback) {
network_responder_->RegisterScriptResponse(kBiddingUrlPath,
BasicBiddingReportScript());
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
network_responder_->RegisterReportResponse("/report_bidder", /*response=*/"");
network_responder_->RegisterReportResponse("/report_seller", /*response=*/"");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
// Nothing should happen until the URN's callback is invoked.
task_environment()->FastForwardBy(base::Days(1));
EXPECT_EQ(network_responder_->ReportCount(), 0u);
InvokeCallbackForURN(*auction_result);
network_responder_->WaitForNumReports(2);
}
// Make sure the report-sending logic can handle two auctions with a delay
// between them. This is regression test for https://crbug.com/1379234.
TEST_F(AdAuctionServiceImplTest, SendReportsTwoAuctionsWithDelay) {
network_responder_->RegisterScriptResponse(kBiddingUrlPath,
BasicBiddingReportScript());
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
network_responder_->RegisterReportResponse("/report_bidder", /*response=*/"");
network_responder_->RegisterReportResponse("/report_seller", /*response=*/"");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(2);
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
network_responder_->WaitForNumReports(2u);
// Chrome runs for a day.
task_environment()->FastForwardBy(base::Days(1));
// Should have been no other pending reports.
EXPECT_EQ(network_responder_->ReportCount(), 2u);
// Re-running the auction should result in 2 more reports.
auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
network_responder_->WaitForNumReports(4u);
}
// Test that if one auction completes after another's reports have been sent,
// but before the report interval has elapsed, its requests still respect the
// report interval.
TEST_F(AdAuctionServiceImplTest, SendReportsTwoAuctionsRespectsReportInterval) {
manager_->set_reporting_interval_for_testing(base::Seconds(5));
manager_->set_max_active_report_requests_for_testing(1);
network_responder_->RegisterScriptResponse(kBiddingUrlPath,
BasicBiddingReportScript());
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
network_responder_->RegisterReportResponse("/report_bidder", /*response=*/"");
network_responder_->RegisterReportResponse("/report_seller", /*response=*/"");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(2);
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
base::Time start_time = base::Time::Now();
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
// First report should be sent immediately.
network_responder_->WaitForNumReports(1u);
EXPECT_EQ(base::TimeDelta(), base::Time::Now() - start_time);
// Second report should be sent after the reporting interval.
network_responder_->WaitForNumReports(2u);
EXPECT_EQ(base::Seconds(5), base::Time::Now() - start_time);
// Time passes that's less than the reporting interval. This shouldn't have
// any effect on when the next report is sent. That is, the reporting interval
// for the next report starts when the previous report was sent, not when the
// next report is queued.
task_environment()->FastForwardBy(base::Seconds(2));
// Re-running the auction should result in 2 more reports.
auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
// The third report (first report for the second auction) should be only be
// sent after the reporting interval has elapsed again, starting from when the
// second report was sent.
network_responder_->WaitForNumReports(3u);
EXPECT_EQ(base::Seconds(10), base::Time::Now() - start_time);
// Second report of the second auction should wait for yet another reporting
// interval.
network_responder_->WaitForNumReports(4u);
EXPECT_EQ(base::Seconds(15), base::Time::Now() - start_time);
}
// Similar to SendReports() above, but with one report request failed instead of
// timed out. Following report requests should still be send after previous ones
// failed.
TEST_F(AdAuctionServiceImplTest, SendReportsOneReportFailed) {
manager_->set_reporting_interval_for_testing(base::Seconds(5));
manager_->set_max_active_report_requests_for_testing(1);
network_responder_->RegisterScriptResponse(kBiddingUrlPath,
BasicBiddingReportScript());
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
network_responder_->RegisterReportResponse("/report_bidder", /*response=*/"");
network_responder_->FailRequestWithError("/report_seller",
net::ERR_CONNECTION_FAILED);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
// There should be no report since the seller report failed, and the bidder
// report request is not fetched yet.
EXPECT_EQ(network_responder_->ReportCount(), 0u);
task_environment()->FastForwardBy(base::Seconds(2));
// The next request will not be sent when it's less than 5 seconds after the
// previous request completed.
EXPECT_EQ(network_responder_->ReportCount(), 0u);
task_environment()->FastForwardBy(base::Seconds(4));
// The bidder's report request was fetched after 5 seconds since the previous
// request completed.
EXPECT_EQ(network_responder_->ReportCount(), 1u);
}
// Checks that all reporting in the pending queue gets canceled if the reporting
// queue max length is exceeded at the time of enqueuing a new set of reports.
TEST_F(AdAuctionServiceImplTest, ReportQueueMaxLength) {
// Use interest group name as bid value.
const std::string kBiddingScript = base::StringPrintf(R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {
'ad': 'example',
'bid': parseInt(interestGroup.name),
'render': 'https://example.com/render'
};
}
function reportWin(
auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
sendReportTo('%s/report_bidder_' + browserSignals.bid);
}
)",
kOriginStringA);
const std::string kDecisionScript =
base::StringPrintf(R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('%s/report_seller_' + browserSignals.bid);
return {
'success': true,
'signalsForWinner': {'signalForWinner': 1},
'reportUrl': '%s/report_seller_' + browserSignals.bid,
};
}
)",
kOriginStringA, kOriginStringA);
// Set the global maximum queue length to the number of reports each auction
// run below tries to make (which is 2), so the second report made by each
// auction won't evict the first request.
//
// Use 3 instead of 2 because the queue is truncated when the "max" is hit by
// appending a report, as opposed to when the "max" is exceeded.
manager_->set_max_report_queue_length_for_testing(3);
manager_->set_max_active_report_requests_for_testing(1);
manager_->set_reporting_interval_for_testing(base::Seconds(5));
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
// Run three auctions, each time with a new interest group which bids i wins
// the auction.
for (int i = 1; i < 4; i++) {
const std::string name = base::NumberToString(i);
network_responder_->RegisterReportResponse(
base::StringPrintf("/report_bidder_%s", name.c_str()), "");
network_responder_->RegisterReportResponse(
base::StringPrintf("/report_seller_%s", name.c_str()), "");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.name = name;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, name));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
// Wait for the reporting scripts to complete and reporting URLs to be
// requested. Need to do this for each auction to make sure reporting
// scripts complete and requests are made in a consistent order.
task_environment()->RunUntilIdle();
}
// There should be one report sent already, since there's no delay for the
// first report.
EXPECT_EQ(network_responder_->ReportCount(), 1u);
EXPECT_TRUE(network_responder_->ReportSent("/report_seller_1"));
// Fastforward enough time for all expected reports to be sent.
task_environment()->FastForwardBy(base::Seconds(60));
// Two more reports were sent.
EXPECT_EQ(network_responder_->ReportCount(), 3u);
// The last (third) auction's reports should be sent successfully.
EXPECT_TRUE(network_responder_->ReportSent("/report_bidder_3"));
EXPECT_TRUE(network_responder_->ReportSent("/report_seller_3"));
// The first auction's second report and the second auction's reports should
// be dropped and not be sent.
EXPECT_FALSE(network_responder_->ReportSent("/report_bidder_1"));
EXPECT_FALSE(network_responder_->ReportSent("/report_bidder_2"));
EXPECT_FALSE(network_responder_->ReportSent("/report_seller_2"));
}
TEST_F(AdAuctionServiceImplTest, SendReportsMaxReportRoundDuration) {
// `max_reporting_round_duration_` is set lower than `reporting_interval_` so
// that we can exceed the max round duration with pending unsent reports.
manager_->set_reporting_interval_for_testing(base::Seconds(5));
manager_->set_max_reporting_round_duration_for_testing(base::Seconds(1));
manager_->set_max_active_report_requests_for_testing(1);
network_responder_->RegisterScriptResponse(kBiddingUrlPath,
BasicBiddingReportScript());
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
network_responder_->RegisterReportResponse("/report_bidder", /*response=*/"");
network_responder_->RegisterReportResponse("/report_seller", /*response=*/"");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
// Wait for the reporting scripts to complete and all reporting URLs to be
// requested.
task_environment()->RunUntilIdle();
// Wait for the seller report to be sent.
network_responder_->WaitForNumReports(1);
// The bidder report request should still be waiting in the report queue.
EXPECT_EQ(manager_->report_queue_length_for_testing(), 1u);
// Run a second auction while the first auction's reporting is in progress.
blink::AuctionConfig auction_config2;
auction_config2.seller = kOriginA;
auction_config2.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config2.non_shared_params.interest_group_buyers = {kOriginA};
auction_result = RunAdAuctionAndFlush(auction_config2);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
// Wait for the reporting scripts to complete and all reporting URLs to be
// requested.
task_environment()->RunUntilIdle();
// Two more reports are enqueued.
EXPECT_EQ(manager_->report_queue_length_for_testing(), 3u);
task_environment()->FastForwardBy(base::Seconds(20));
// Should still only have 1 report sent because the report queue is cleared
// after `max_reporting_round_duration` passed, before popping the second
// report from the queue and send it.
EXPECT_EQ(network_responder_->ReportCount(), 1u);
EXPECT_EQ(manager_->report_queue_length_for_testing(), 0u);
// Set `max_reporting_round_duration_` high enough so that the auction's two
// reports can be sent successfully.
manager_->set_max_reporting_round_duration_for_testing(base::Seconds(20));
// Run a third auction after report queue is cleared, to make sure further
// auction's reports can be normally enqueued and sent again.
blink::AuctionConfig auction_config3;
auction_config3.seller = kOriginA;
auction_config3.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config3.non_shared_params.interest_group_buyers = {kOriginA};
auction_result = RunAdAuctionAndFlush(auction_config3);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
// Wait for the reporting scripts to complete and all reporting URLs to be
// requested.
task_environment()->RunUntilIdle();
task_environment()->FastForwardBy(base::Seconds(20));
// Two more reports from the third auction are sent.
EXPECT_EQ(network_responder_->ReportCount(), 3u);
}
// Check that running reporting worklets doesn't block auction completion. To do
// this, the bidding script is set to be deferred. The auction is started, and
// the bid script is supplied. Then the auction completes. This should trigger
// reloading the bidding script to call reportWin(). The second time, a bidding
// script is not supplied. The fact that the auction completes despite the
// second stalled load verifies that running reporting scripts does not block
// completion of an auction. The AdAuctionServiceImpl is destroyed before
// the bidding URL is downloaded the second time, which provides some test
// coverage of that as well.
TEST_F(AdAuctionServiceImplTest, ReportingWorkletsDoNotBlockCompletion) {
network_responder_->RegisterDeferredScriptResponse(kBiddingUrlPath);
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
network_responder_->RegisterReportResponse("/report_bidder", /*response=*/"");
network_responder_->RegisterReportResponse("/report_seller", /*response=*/"");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), ad_auction_service_.BindNewPipeAndPassReceiver());
// Start the auction.
base::RunLoop run_loop;
std::optional<blink::FencedFrame::RedactedFencedFrameConfig> maybe_config;
ad_auction_service_->RunAdAuction(
auction_config, mojo::NullReceiver(),
base::BindLambdaForTesting(
[&run_loop, &maybe_config](
bool aborted_by_script,
const std::optional<
blink::FencedFrame::RedactedFencedFrameConfig>& config) {
EXPECT_FALSE(aborted_by_script);
maybe_config = config;
run_loop.Quit();
}));
// Wait for the NetworkResponder to see the request for the bidding URL, and
// response with the bidding script.
task_environment()->RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
network_responder_->DoDeferredScriptResponse(kBiddingUrlPath,
BasicBiddingReportScript());
// Register another deferred response for when the bidding URL is requested
// again to run the reporting script.
network_responder_->RegisterDeferredScriptResponse(kBiddingUrlPath);
// Complete the auction. It should have a winning ad.
run_loop.Run();
ASSERT_TRUE(maybe_config);
EXPECT_TRUE(maybe_config->urn_uuid().has_value());
// Running until idle should result in the NetworkResponder receiving a
// request for the bidding URL, to run the reporting script.
task_environment()->RunUntilIdle();
EXPECT_TRUE(network_responder_->HasPendingResponse(kBiddingUrlPath));
// Destroy the auction service Mojo pipe, and wait for the underlying service
// to be destroyed, which should not cause a crash.
DestroyAdAuctionService();
base::RunLoop().RunUntilIdle();
}
// Run several auctions, some of which have a winner, and some of which do
// not. Verify that the auction result UMA is recorded correctly.
TEST_F(AdAuctionServiceImplTest,
AddInterestGroupRunAuctionVerifyResultMetrics) {
base::HistogramTester histogram_tester;
constexpr char kDecisionFailAllUrlPath[] =
"/interest_group/decision_logic_fail_all.js";
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
function reportResult() {}
)";
constexpr char kDecisionScriptFailAll[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return 0;
}
function reportResult() {}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
network_responder_->RegisterScriptResponse(kDecisionFailAllUrlPath,
kDecisionScriptFailAll);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(10);
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Run 7 auctions, with delays:
//
// succeed, (1s), fail, (3s), succeed, (1m), succeed, (10m) succeed, (30m)
// fail, (1h), fail, which in bits (with an extra leading 1) is 0b1101110 --
// the last failure isn't recorded in the bitfield, since only the first 6
// auctions get recorded in the bitfield.
// Expect*TimeSample() doesn't accept base::TimeDelta::Max(), but the max time
// bucket size is 1 hour, so specifying kMaxTime will select the max bucket.
constexpr base::TimeDelta kMaxTime{base::Days(1)};
blink::AuctionConfig succeed_auction_config;
succeed_auction_config.seller = kOriginA;
succeed_auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
succeed_auction_config.non_shared_params.interest_group_buyers = {kOriginA};
blink::AuctionConfig fail_auction_config;
fail_auction_config.seller = kOriginA;
fail_auction_config.decision_logic_url =
kUrlA.Resolve(kDecisionFailAllUrlPath);
fail_auction_config.non_shared_params.interest_group_buyers = {kOriginA};
// 1st auction
EXPECT_NE(RunAdAuctionAndFlush(succeed_auction_config), std::nullopt);
// Time metrics are published every auction.
histogram_tester.ExpectUniqueTimeSample(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", kMaxTime, 1);
// 2nd auction
task_environment()->FastForwardBy(base::Seconds(1));
EXPECT_EQ(RunAdAuctionAndFlush(fail_auction_config), std::nullopt);
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", base::Seconds(1),
1);
// 3rd auction
task_environment()->FastForwardBy(base::Seconds(3));
EXPECT_NE(RunAdAuctionAndFlush(succeed_auction_config), std::nullopt);
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", base::Seconds(3),
1);
// 4th auction
task_environment()->FastForwardBy(base::Minutes(1));
EXPECT_NE(RunAdAuctionAndFlush(succeed_auction_config), std::nullopt);
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", base::Minutes(1),
1);
// 5th auction
task_environment()->FastForwardBy(base::Minutes(10));
EXPECT_NE(RunAdAuctionAndFlush(succeed_auction_config), std::nullopt);
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage",
base::Minutes(10), 1);
// 6th auction
task_environment()->FastForwardBy(base::Minutes(30));
EXPECT_EQ(RunAdAuctionAndFlush(fail_auction_config), std::nullopt);
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage",
base::Minutes(30), 1);
// 7th auction
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(RunAdAuctionAndFlush(fail_auction_config), std::nullopt);
// Since the 1st auction has no prior auction -- it gets put in the same
// bucket with the 7th auction -- there are 2 auctions now in this bucket.
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", kMaxTime, 2);
// Some metrics only get reported until after navigation.
EXPECT_EQ(histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.NumAuctionsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.First6AuctionsBitsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit")
.size(),
0u);
// DeleteContents() to force-populate remaining metrics.
DeleteContents();
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsPerPage", 7, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage", 4 * 100 / 7,
1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.First6AuctionsBitsPerPage", 0b1101110, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit", 0, 1);
}
// Like AddInterestGroupRunAuctionVerifyResultMetrics, but with a smaller number
// of auctions -- this verifies that metrics (especially the bit metrics) are
// reported correctly in this scenario.
TEST_F(AdAuctionServiceImplTest,
AddInterestGroupRunAuctionVerifyResultMetricsFewAuctions) {
base::HistogramTester histogram_tester;
constexpr char kDecisionFailAllUrlPath[] =
"/interest_group/decision_logic_fail_all.js";
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
function reportResult() {}
)";
constexpr char kDecisionScriptFailAll[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return 0;
}
function reportResult() {}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
network_responder_->RegisterScriptResponse(kDecisionFailAllUrlPath,
kDecisionScriptFailAll);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(10);
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Run 2 auctions, with delays:
//
// succeed, (1s), fail, which in bits (with an extra leading 1) is 0b110.
// Expect*TimeSample() doesn't accept base::TimeDelta::Max(), but the max time
// bucket size is 1 hour, so specifying kMaxTime will select the max bucket.
constexpr base::TimeDelta kMaxTime{base::Days(1)};
blink::AuctionConfig succeed_auction_config;
succeed_auction_config.seller = kOriginA;
succeed_auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
succeed_auction_config.non_shared_params.interest_group_buyers = {kOriginA};
blink::AuctionConfig fail_auction_config;
fail_auction_config.seller = kOriginA;
fail_auction_config.decision_logic_url =
kUrlA.Resolve(kDecisionFailAllUrlPath);
fail_auction_config.non_shared_params.interest_group_buyers = {kOriginA};
// 1st auction
EXPECT_NE(RunAdAuctionAndFlush(succeed_auction_config), std::nullopt);
// Time metrics are published every auction.
histogram_tester.ExpectUniqueTimeSample(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", kMaxTime, 1);
// 2nd auction
task_environment()->FastForwardBy(base::Seconds(1));
EXPECT_EQ(RunAdAuctionAndFlush(fail_auction_config), std::nullopt);
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", base::Seconds(1),
1);
// Some metrics only get reported until after navigation.
EXPECT_EQ(histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.NumAuctionsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.First6AuctionsBitsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit")
.size(),
0u);
// DeleteContents() to force-populate remaining metrics.
DeleteContents();
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsPerPage", 2, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage", 1 * 100 / 2,
1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.First6AuctionsBitsPerPage", 0b110, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit", 0, 1);
}
// Like AddInterestGroupRunAuctionVerifyResultMetricsFewAuctions, but with no
// auctions.
TEST_F(AdAuctionServiceImplTest,
AddInterestGroupRunAuctionVerifyResultMetricsNoAuctions) {
base::HistogramTester histogram_tester;
// Don't run any auctions.
// Navigate to "populate" remaining metrics.
DeleteContents();
// Nothing gets reported since there were no auctions.
EXPECT_EQ(histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.NumAuctionsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.First6AuctionsBitsPerPage")
.size(),
0u);
EXPECT_EQ(histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit")
.size(),
0u);
}
// The feature parameter that controls the interest group limit should default
// to off. We both check the parameter is off, and we run a number of auctions
// and make sure they all succeed.
TEST_F(AdAuctionServiceImplTest, NoInterestLimitByDefault) {
EXPECT_FALSE(base::FeatureList::IsEnabled(features::kFledgeLimitNumAuctions));
base::HistogramTester histogram_tester;
constexpr char kDecisionFailAllUrlPath[] =
"/interest_group/decision_logic_fail_all.js";
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
function reportResult() {}
)";
constexpr char kDecisionScriptFailAll[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return 0;
}
function reportResult() {}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
network_responder_->RegisterScriptResponse(kDecisionFailAllUrlPath,
kDecisionScriptFailAll);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(10);
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
constexpr int kNumAuctions = 10;
// Run kNumAuctions auctions, all should succeed since there's no limit:
blink::AuctionConfig succeed_auction_config;
succeed_auction_config.seller = kOriginA;
succeed_auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
succeed_auction_config.non_shared_params.interest_group_buyers = {kOriginA};
for (int i = 0; i < kNumAuctions; i++) {
EXPECT_NE(RunAdAuctionAndFlush(succeed_auction_config), std::nullopt);
}
// Some metrics only get reported until after navigation.
EXPECT_EQ(histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.NumAuctionsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.First6AuctionsBitsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit")
.size(),
0u);
// DeleteContents() to force-populate remaining metrics.
DeleteContents();
// Every auction succeeds, none are skipped.
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsPerPage", kNumAuctions, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage", 100, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.First6AuctionsBitsPerPage", 0b1111111, 1);
// However, we do record that the auction was skipped.
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit", 0, 1);
}
// CreateAdRequest should reject if we have an empty config.
TEST_F(AdAuctionServiceImplTest, CreateAdRequestRejectsEmptyConfigRequest) {
auto mojo_config = blink::mojom::AdRequestConfig::New();
bool callback_fired = false;
CreateAdRequest(std::move(mojo_config),
base::BindLambdaForTesting(
[&](const std::optional<std::string>& ads_guid) {
ASSERT_FALSE(ads_guid.has_value());
callback_fired = true;
}));
ASSERT_TRUE(callback_fired);
}
// CreateAdRequest should reject if we have an otherwise okay request but our
// request URL is not using HTTPS.
TEST_F(AdAuctionServiceImplTest, CreateAdRequestRejectsHttpUrls) {
auto mojo_config = blink::mojom::AdRequestConfig::New();
mojo_config->ad_request_url = GURL("http://site.test/");
auto mojo_ad_properties = blink::mojom::AdProperties::New();
mojo_ad_properties->width = "48";
mojo_ad_properties->height = "64";
mojo_ad_properties->slot = "123";
mojo_ad_properties->lang = "en";
mojo_ad_properties->ad_type = "test";
mojo_ad_properties->bid_floor = 1.0;
mojo_config->ad_properties.push_back(std::move(mojo_ad_properties));
bool callback_fired = false;
CreateAdRequest(std::move(mojo_config),
base::BindLambdaForTesting(
[&](const std::optional<std::string>& ads_guid) {
ASSERT_FALSE(ads_guid.has_value());
callback_fired = true;
}));
ASSERT_TRUE(callback_fired);
}
// CreateAdRequest should reject if we have an otherwise okay request but no ad
// properties.
TEST_F(AdAuctionServiceImplTest, CreateAdRequestRejectsMissingAds) {
auto mojo_config = blink::mojom::AdRequestConfig::New();
mojo_config->ad_request_url = GURL("https://site.test/");
bool callback_fired = false;
CreateAdRequest(std::move(mojo_config),
base::BindLambdaForTesting(
[&](const std::optional<std::string>& ads_guid) {
ASSERT_FALSE(ads_guid.has_value());
callback_fired = true;
}));
ASSERT_TRUE(callback_fired);
}
// CreateAdRequest should reject if we have an otherwise okay request but
// include an HTTP fallback URL.
TEST_F(AdAuctionServiceImplTest, CreateAdRequestRejectsHttpFallback) {
auto mojo_config = blink::mojom::AdRequestConfig::New();
mojo_config->ad_request_url = GURL("https://site.test/");
auto mojo_ad_properties = blink::mojom::AdProperties::New();
mojo_ad_properties->width = "48";
mojo_ad_properties->height = "64";
mojo_ad_properties->slot = "123";
mojo_ad_properties->lang = "en";
mojo_ad_properties->ad_type = "test";
mojo_ad_properties->bid_floor = 1.0;
mojo_config->ad_properties.push_back(std::move(mojo_ad_properties));
mojo_config->fallback_source = GURL("http://fallback_site.test/");
bool callback_fired = false;
CreateAdRequest(std::move(mojo_config),
base::BindLambdaForTesting(
[&](const std::optional<std::string>& ads_guid) {
ASSERT_FALSE(ads_guid.has_value());
callback_fired = true;
}));
ASSERT_TRUE(callback_fired);
}
// An empty config should be treated as a bad message.
TEST_F(AdAuctionServiceImplTest, FinalizeAdRejectsEmptyConfig) {
blink::AuctionConfig config;
FinalizeAdAndExpectPipeClosed(
/*guid=*/std::string("1234"), config);
}
// An HTTP decision logic URL should be treated as a bad message.
TEST_F(AdAuctionServiceImplTest, FinalizeAdRejectsHTTPDecisionUrl) {
blink::AuctionConfig config;
config.seller = url::Origin::Create(GURL("https://site.test"));
config.decision_logic_url = GURL("http://site.test/");
FinalizeAdAndExpectPipeClosed(
/*guid=*/"1234", config);
}
// An empty GUID should be treated as a bad message.
TEST_F(AdAuctionServiceImplTest, FinalizeAdRejectsMissingGuid) {
blink::AuctionConfig config;
config.seller = url::Origin::Create(GURL("https://site.test"));
config.decision_logic_url = GURL("https://site.test/");
FinalizeAdAndExpectPipeClosed(
/*guid=*/std::string(), config);
}
TEST_F(AdAuctionServiceImplTest, SetPriorityAdjustsPriority) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
if (interestGroup.priority !== undefined)
throw new Error("Priority should not be in worklet");
setPriority(99);
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(2, GetPriority(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render"));
EXPECT_EQ(99, GetPriority(kOriginA, kInterestGroupName));
}
class AdAuctionServiceImplNumAuctionLimitTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplNumAuctionLimitTest() {
// Only 2 auctions are allowed per-page.
feature_list_.InitAndEnableFeatureWithParameters(
features::kFledgeLimitNumAuctions, {{"max_auctions_per_page", "2"}});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Like AddInterestGroupRunAuctionVerifyResultMetrics, but with enforcement
// limiting the number of auctions.
TEST_F(AdAuctionServiceImplNumAuctionLimitTest,
AddInterestGroupRunAuctionWithNumAuctionLimits) {
base::HistogramTester histogram_tester;
constexpr char kDecisionFailAllUrlPath[] =
"/interest_group/decision_logic_fail_all.js";
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
function reportResult() {}
)";
constexpr char kDecisionScriptFailAll[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return 0;
}
function reportResult() {}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
network_responder_->RegisterScriptResponse(kDecisionFailAllUrlPath,
kDecisionScriptFailAll);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(10);
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
// Run 3 auctions, with delays:
//
// succeed, (1s), fail, (3s), succeed which in bits (with an extra leading 1)
// is 0b110 -- the last success isn't recorded since the auction limit is
// enforced.
// Expect*TimeSample() doesn't accept base::TimeDelta::Max(), but the max time
// bucket size is 1 hour, so specifying kMaxTime will select the max bucket.
constexpr base::TimeDelta kMaxTime{base::Days(1)};
blink::AuctionConfig succeed_auction_config;
succeed_auction_config.seller = kOriginA;
succeed_auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
succeed_auction_config.non_shared_params.interest_group_buyers = {kOriginA};
blink::AuctionConfig fail_auction_config;
fail_auction_config.seller = kOriginA;
fail_auction_config.decision_logic_url =
kUrlA.Resolve(kDecisionFailAllUrlPath);
fail_auction_config.non_shared_params.interest_group_buyers = {kOriginA};
// 1st auction
EXPECT_NE(RunAdAuctionAndFlush(succeed_auction_config), std::nullopt);
// Time metrics are published every auction.
histogram_tester.ExpectUniqueTimeSample(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", kMaxTime, 1);
// 2nd auction
task_environment()->FastForwardBy(base::Seconds(1));
EXPECT_EQ(RunAdAuctionAndFlush(fail_auction_config), std::nullopt);
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", base::Seconds(1),
1);
// 3rd auction -- fails even though decision_logic.js is used because the
// auction limit is encountered.
task_environment()->FastForwardBy(base::Seconds(3));
EXPECT_EQ(RunAdAuctionAndFlush(succeed_auction_config), std::nullopt);
// The time metrics shouldn't get updated.
histogram_tester.ExpectTimeBucketCount(
"Ads.InterestGroup.Auction.TimeSinceLastAuctionPerPage", base::Seconds(3),
0);
// Some metrics only get reported until after navigation.
EXPECT_EQ(histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.NumAuctionsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples("Ads.InterestGroup.Auction.First6AuctionsBitsPerPage")
.size(),
0u);
EXPECT_EQ(
histogram_tester
.GetAllSamples(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit")
.size(),
0u);
// DeleteContents() to force-populate remaining metrics.
DeleteContents();
// The last auction doesn't count towards these metrics since the auction
// limit is enforced -- this is because that auction doesn't contribute any
// knowledge about stored interest groups to the page.
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsPerPage", 2, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage", 1 * 100 / 2,
1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.First6AuctionsBitsPerPage", 0b110, 1);
// However, we do record that the auction was skipped.
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit", 1, 1);
}
TEST_F(AdAuctionServiceImplNumAuctionLimitTest,
AddInterestGroupRunAuctionStartManyAuctionsInParallel) {
base::HistogramTester histogram_tester;
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
function reportResult() {}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(10);
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig succeed_auction_config;
succeed_auction_config.seller = kOriginA;
succeed_auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
succeed_auction_config.non_shared_params.interest_group_buyers = {kOriginA};
// Pick some large number, larger than the auction limit.
constexpr int kNumAuctions = 10;
base::RunLoop run_loop;
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RepeatingClosure one_auction_complete =
base::BarrierClosure(kNumAuctions, run_loop.QuitClosure());
for (int i = 0; i < kNumAuctions; i++) {
interest_service->RunAdAuction(
succeed_auction_config, mojo::NullReceiver(),
base::BindLambdaForTesting(
[&one_auction_complete](
bool aborted_by_script,
const std::optional<
blink::FencedFrame::RedactedFencedFrameConfig>&
ignored_config) { one_auction_complete.Run(); }));
}
run_loop.Run();
// DeleteContents() to force-populate remaining metrics.
DeleteContents();
// Only the first 2 auctions should have succeeded -- the others should fail.
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsPerPage", 2, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.PercentAuctionsSuccessfulPerPage", 2 * 100 / 2,
1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.First6AuctionsBitsPerPage", 0b111, 1);
histogram_tester.ExpectUniqueSample(
"Ads.InterestGroup.Auction.NumAuctionsSkippedDueToAuctionLimit",
kNumAuctions - 2, 1);
}
class AdAuctionServiceImplRestrictedPermissionsPolicyTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplRestrictedPermissionsPolicyTest() {
feature_list_.InitAndEnableFeature(
blink::features::kAdInterestGroupAPIRestrictedPolicyByDefault);
blink::UpdatePermissionsPolicyFeatureListForTesting();
old_content_browser_client_ =
SetBrowserClientForTesting(&content_browser_client_);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Permissions policy feature join-ad-interest-group is enabled by default for
// top level frames under restricted permissions policy, so interest group
// APIs should succeed.
TEST_F(AdAuctionServiceImplRestrictedPermissionsPolicyTest,
APICallsFromTopFrame) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({"biddingLogicURL": "%s%s"})",
kOriginStringA, kNewBiddingUrlPath));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
ASSERT_TRUE(group.bidding_url.has_value());
EXPECT_EQ(group.bidding_url->spec(),
base::StringPrintf("%s%s", kOriginStringA, kNewBiddingUrlPath));
LeaveInterestGroupAndFlush(kOriginA, kInterestGroupName);
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
}
// Like APICallsFromTopFrame, but API calls happens in a same site iframe
// instead of a top frame.
TEST_F(AdAuctionServiceImplRestrictedPermissionsPolicyTest,
APICallsFromSameSiteIframe) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({"biddingLogicURL": "%s%s"})",
kOriginStringA, kNewBiddingUrlPath));
// Create a same site subframe and use it to send the interest group requests.
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
subframe =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlA, subframe);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
JoinInterestGroupAndFlush(std::move(interest_group), subframe);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlushForFrame(subframe);
task_environment()->RunUntilIdle();
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
ASSERT_TRUE(group.bidding_url.has_value());
EXPECT_EQ(group.bidding_url->spec(),
base::StringPrintf("%s%s", kOriginStringA, kNewBiddingUrlPath));
LeaveInterestGroupAndFlush(kOriginA, kInterestGroupName, subframe);
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
}
// Permissions policy feature join-ad-interest-group is disabled by default for
// cross site iframes under restricted permissions policy, so interest group
// APIs should not work, and result in the pipe being closed.
TEST_F(AdAuctionServiceImplRestrictedPermissionsPolicyTest,
APICallsFromCrossSiteIFrame) {
network_responder_->RegisterUpdateResponse(
kUpdateUrlPath, base::StringPrintf(R"({"biddingLogicURL": "%s%s"})",
kOriginStringC, kNewBiddingUrlPath));
// Join an interest group for origin C from an origin A URL. Do it manually
// to bypass permissions checks. It's important to join it from an A URL to
// make sure that the ClearOriginJoinedInterestGroups() has no effect,
// in addition to the pipe being closed.
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = kOriginC;
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
manager_->JoinInterestGroup(interest_group, kUrlA);
NavigateAndCommit(kUrlA);
EXPECT_EQ(1, GetJoinCount(kOriginC, kInterestGroupName));
// Create a cross site subframe and use it to send interest group requests.
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
subframe =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlC, subframe);
blink::InterestGroup interest_group_2 = CreateInterestGroup();
constexpr char kInterestGroupName2[] = "group2";
interest_group.owner = kOriginC;
interest_group.name = kInterestGroupName2;
JoinInterestGroupAndExpectBadMessage(
std::move(interest_group_2),
"Unexpected request: Interest groups may only be joined or left when "
"feature join-ad-interest-group is enabled by Permissions Policy",
subframe);
EXPECT_EQ(0, GetJoinCount(kOriginC, kInterestGroupName2));
UpdateInterestGroupNoFlushForFrame(subframe);
task_environment()->RunUntilIdle();
// `bidding_url` should not change.
scoped_refptr<StorageInterestGroups> groups =
GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
EXPECT_EQ(group.name, kInterestGroupName);
ASSERT_TRUE(group.bidding_url.has_value());
EXPECT_EQ(group.bidding_url->spec(),
base::StringPrintf("%s%s", kOriginStringC, kBiddingUrlPath));
LeaveInterestGroupAndExpectBadMessage(
kOriginC, kInterestGroupName,
"Unexpected request: Interest groups may only be joined or left when "
"feature join-ad-interest-group is enabled by Permissions Policy",
subframe);
EXPECT_EQ(1, GetJoinCount(kOriginC, kInterestGroupName));
ClearOriginJoinedInterestGroupsAndExpectBadMessage(
kOriginC,
"Unexpected request: Interest groups may only be joined or left when "
"feature join-ad-interest-group is enabled by Permissions Policy",
subframe);
EXPECT_EQ(1, GetJoinCount(kOriginC, kInterestGroupName));
}
class AdAuctionServiceImplBiddingAndScoringDebugReportingAPIEnabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplBiddingAndScoringDebugReportingAPIEnabledTest() {
feature_list_.InitAndEnableFeature(
blink::features::kBiddingAndScoringDebugReportingAPI);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Allowing sending multiple reports in parallel, instead of only allowing
// sending one at a time.
TEST_F(AdAuctionServiceImplBiddingAndScoringDebugReportingAPIEnabledTest,
SendReportsMaximumActive) {
// Use interest group name as bid value.
const std::string kBiddingScript =
base::StringPrintf(R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
forDebuggingOnly.reportAdAuctionWin(
`%s/bidder_debug_win_` + interestGroup.name);
return {
'ad': 'example',
'bid': parseInt(interestGroup.name),
'render': 'https://example.com/render'
};
}
function reportWin(
auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
sendReportTo('%s/report_bidder_' + browserSignals.bid);
}
)",
kOriginStringA, kOriginStringA);
const std::string kDecisionScript =
base::StringPrintf(R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
forDebuggingOnly.reportAdAuctionWin(`%s/seller_debug_win_` + bid);
return bid;
}
function reportResult(auctionConfig, browserSignals) {
const reportUrl = '%s/report_seller_' + browserSignals.bid;
sendReportTo(reportUrl);
return {
'success': true,
'signalsForWinner': {'signalForWinner': 1},
'reportUrl': reportUrl,
};
}
)",
kOriginStringA, kOriginStringA);
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(3);
manager_->set_reporting_interval_for_testing(base::Seconds(5));
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
// Run two auctions, each time with a new interest group which bids i wins
// the auction.
for (int i = 1; i < 3; i++) {
const std::string name = base::NumberToString(i);
network_responder_->RegisterReportResponse(
base::StringPrintf("/report_bidder_%s", name.c_str()), /*response=*/"");
network_responder_->RegisterReportResponse(
base::StringPrintf("/report_seller_%s", name.c_str()), /*response=*/"");
network_responder_->RegisterReportResponse(
base::StringPrintf("/seller_debug_win_%s", name.c_str()),
/*response=*/"");
network_responder_->RegisterReportResponse(
base::StringPrintf("/bidder_debug_win_%s", name.c_str()),
/*response=*/"");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.name = name;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, name));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
// Wait for the reporting scripts to complete. Doing this before invoking
// the navigation callback results in reports being sent in the order:
// * Seller reportResult() report.
// * Bidder reportWin() report.
// * Win reports.
//
// Not doing this results in more confusing orders, with the seller report
// potentially being sent either before or after the debug reports, which
// are always sent before the bidder report.
task_environment()->RunUntilIdle();
// This will cause the reports to be queued.
InvokeCallbackForURN(*auction_result);
}
task_environment()->FastForwardBy(base::Seconds(3));
// Three reports sent already. Reporting interval is set to 5s, and only 3s
// passed, so no next report was sent after one report was sent, i.e., all
// sent reports were sent in the same round in parallel.
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/report_seller_1"));
EXPECT_TRUE(network_responder_->ReportSent("/report_bidder_1"));
EXPECT_TRUE(network_responder_->ReportSent("/bidder_debug_win_1"));
// Fastforward to pass reporting interval (but less than two reporting
// intervals) so that the second round of reports are sent but the third
// round hasn't started.
task_environment()->FastForwardBy(base::Seconds(5));
// Three more reports were sent.
EXPECT_EQ(network_responder_->ReportCount(), 6u);
EXPECT_TRUE(network_responder_->ReportSent("/seller_debug_win_1"));
EXPECT_TRUE(network_responder_->ReportSent("/report_seller_2"));
EXPECT_TRUE(network_responder_->ReportSent("/report_bidder_2"));
// Fastforward enough time for all reports to be sent.
task_environment()->FastForwardBy(base::Seconds(6));
EXPECT_EQ(network_responder_->ReportCount(), 8u);
EXPECT_TRUE(network_responder_->ReportSent("/bidder_debug_win_2"));
EXPECT_TRUE(network_responder_->ReportSent("/seller_debug_win_2"));
}
class AdAuctionServiceImplEventReportingAttestationTest
: public AdAuctionServiceImplBiddingAndScoringDebugReportingAPIEnabledTest {
public:
// Run an auction with 2 interest groups, and send reports to different
// third-party (non seller or buyer) origins.
void RunAuctionAndWaitForReports() {
// Use interest group name as bid value.
const std::string kBiddingScript =
base::StringPrintf(R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
forDebuggingOnly.reportAdAuctionWin(
`%s/bidder_debug_win_` + interestGroup.name);
forDebuggingOnly.reportAdAuctionLoss(
`%s/bidder_debug_loss_` + interestGroup.name);
return {
'ad': 'example',
'bid': parseInt(interestGroup.name),
'render': 'https://example.com/render'
};
}
function reportWin(
auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
sendReportTo('%s/report_bidder_' + browserSignals.bid);
}
)",
kOriginStringB, kOriginStringC, kOriginStringD);
const std::string kDecisionScript =
base::StringPrintf(R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
forDebuggingOnly.reportAdAuctionWin(`%s/seller_debug_win_` + bid);
forDebuggingOnly.reportAdAuctionLoss(`%s/seller_debug_loss_` + bid);
return bid;
}
function reportResult(auctionConfig, browserSignals) {
const reportUrl = '%s/report_seller_' + browserSignals.bid;
sendReportTo(reportUrl);
return {
'success': true,
'signalsForWinner': {'signalForWinner': 1},
'reportUrl': reportUrl,
};
}
)",
kOriginStringE, kOriginStringF, kOriginStringG);
manager_->set_max_report_queue_length_for_testing(50);
manager_->set_max_active_report_requests_for_testing(3);
manager_->set_reporting_interval_for_testing(base::Seconds(5));
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
kDecisionScript);
// Run an auction with 2 interest groups. Interest group 2 wins, and
// interest group 1 loses.
for (int i = 1; i < 3; i++) {
const std::string name = base::NumberToString(i);
network_responder_->RegisterReportResponse(
base::StringPrintf("/report_bidder_%s", name.c_str()),
/*response=*/"");
network_responder_->RegisterReportResponse(
base::StringPrintf("/report_seller_%s", name.c_str()),
/*response=*/"");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.name = name;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, name));
}
network_responder_->RegisterReportResponse("/seller_debug_loss_1",
/*response=*/"");
network_responder_->RegisterReportResponse("/bidder_debug_loss_1",
/*response=*/"");
network_responder_->RegisterReportResponse("/seller_debug_win_2",
/*response=*/"");
network_responder_->RegisterReportResponse("/bidder_debug_win_2",
/*response=*/"");
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
task_environment()->RunUntilIdle();
// This will cause the reports to be queued.
InvokeCallbackForURN(*auction_result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
}
};
// Since all origins are attested in the allowlist, all reports are successfully
// sent.
TEST_F(AdAuctionServiceImplEventReportingAttestationTest, AllAllowed) {
// Allow the below origins to receive event level reports.
content_browser_client_.SetAllowList(
{kOriginB, kOriginC, kOriginD, kOriginE, kOriginF, kOriginG});
RunAuctionAndWaitForReports();
EXPECT_EQ(network_responder_->ReportCount(), 6u);
EXPECT_TRUE(network_responder_->ReportSent("/report_seller_2"));
EXPECT_TRUE(network_responder_->ReportSent("/report_bidder_2"));
EXPECT_TRUE(network_responder_->ReportSent("/bidder_debug_loss_1"));
EXPECT_TRUE(network_responder_->ReportSent("/seller_debug_loss_1"));
EXPECT_TRUE(network_responder_->ReportSent("/bidder_debug_win_2"));
EXPECT_TRUE(network_responder_->ReportSent("/seller_debug_win_2"));
}
// Like EventReportingAttestationAllAllowed, but only some of the report
// destination origins are allowed to receive reports.
TEST_F(AdAuctionServiceImplEventReportingAttestationTest, SomeAllowed) {
// Allow the below origins to receive event level reports.
content_browser_client_.SetAllowList({kOriginB, kOriginD, kOriginF});
RunAuctionAndWaitForReports();
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/report_bidder_2"));
EXPECT_TRUE(network_responder_->ReportSent("/seller_debug_loss_1"));
EXPECT_TRUE(network_responder_->ReportSent("/bidder_debug_win_2"));
}
// Like EventReportingAttestationAllAllowed, but none of the report destination
// origins are allowed to receive reports.
TEST_F(AdAuctionServiceImplEventReportingAttestationTest, NoneAllowed) {
// No origins are allowed to receive event level reports.
content_browser_client_.SetAllowList({});
RunAuctionAndWaitForReports();
EXPECT_EQ(network_responder_->ReportCount(), 0u);
}
// In some scenarios, the `PageImpl` used in auction may change in middle of the
// auction. See MainFrameDocumentAssociatedDataChangesOnSameSiteNavigation in
// SitePerProcessBrowserTest for an example. When this happens, auction must be
// able to detect the change and abort the auction.
//
// See more info about this issue in crbug.com/1422301.
//
// TODO(crbug.com/936696): Once RenderDocument is launched, this issue will be
// resolved, remove this test.
TEST_F(AdAuctionServiceImplTest, PageImplChangedDuringAuction) {
network_responder_->RegisterDeferredScriptResponse(kBiddingUrlPath);
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_gurl=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
ASSERT_NE(
static_cast<RenderFrameHostImpl*>(main_rfh())->auction_initiator_page(),
nullptr);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), ad_auction_service_.BindNewPipeAndPassReceiver());
// Start the auction.
base::RunLoop run_loop;
std::optional<blink::FencedFrame::RedactedFencedFrameConfig> maybe_config;
ad_auction_service_->RunAdAuction(
auction_config, mojo::NullReceiver(),
base::BindLambdaForTesting(
[&run_loop, &maybe_config](
bool aborted_by_script,
const std::optional<
blink::FencedFrame::RedactedFencedFrameConfig>& config) {
EXPECT_FALSE(aborted_by_script);
maybe_config = config;
run_loop.Quit();
}));
// Wait for the NetworkResponder to see the request for the bidding URL, then
// respond.
task_environment()->RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
network_responder_->DoDeferredScriptResponse(kBiddingUrlPath,
BasicBiddingReportScript());
// Simulate invalidating the `PageImpl` used by the auction.
static_cast<RenderFrameHostImpl*>(main_rfh())
->set_auction_initiator_page(nullptr);
// Complete the auction. It should fail due to the `PageImpl` mismatch.
run_loop.Run();
EXPECT_FALSE(maybe_config.has_value());
}
// Similar to PageImplChangedDuringAuction, but the `PageImpl` is changed before
// auction starts.
//
// TODO(crbug.com/936696): Once RenderDocument is launched, remove this test.
TEST_F(AdAuctionServiceImplTest, PageImplChangedBeforeAuction) {
network_responder_->RegisterDeferredScriptResponse(kBiddingUrlPath);
network_responder_->RegisterScriptResponse(kDecisionUrlPath,
BasicSellerReportScript());
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_gurl=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
ASSERT_NE(
static_cast<RenderFrameHostImpl*>(main_rfh())->auction_initiator_page(),
nullptr);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), ad_auction_service_.BindNewPipeAndPassReceiver());
// Simulate invalidating the `PageImpl` used by the auction.
static_cast<RenderFrameHostImpl*>(main_rfh())
->set_auction_initiator_page(nullptr);
// Start the auction.
base::RunLoop run_loop;
std::optional<blink::FencedFrame::RedactedFencedFrameConfig> maybe_config;
ad_auction_service_->RunAdAuction(
auction_config, mojo::NullReceiver(),
base::BindLambdaForTesting(
[&run_loop, &maybe_config](
bool aborted_by_script,
const std::optional<
blink::FencedFrame::RedactedFencedFrameConfig>& config) {
EXPECT_FALSE(aborted_by_script);
maybe_config = config;
run_loop.Quit();
}));
// Try to run the auction. It should fail due to the `PageImpl` mismatch.
task_environment()->RunUntilIdle();
run_loop.Run();
EXPECT_FALSE(maybe_config.has_value());
}
// The weak pointer to the auction initiator page should be reset upon a cross-
// document navigation.
// TODO(crbug.com/936696): Once RenderDocument is launched, remove this test.
TEST_F(AdAuctionServiceImplTest,
ResetAuctionInitiatorPageOnCrossDocumentNavigation) {
if (ShouldCreateNewRenderFrameHostOnSameSiteNavigation(
/*is_main_frame=*/false,
/*is_local_root=*/AreAllSitesIsolatedForTesting())) {
GTEST_SKIP() << "RenderDocument is enabled.";
}
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
subframe =
NavigationSimulator::NavigateAndCommitFromDocument(kUrlA, subframe);
RenderFrameHostImpl* subframe_rfh =
static_cast<RenderFrameHostImpl*>(subframe);
// Act as if there was an infinite unload handler in the subframe.
subframe_rfh->DoNotDeleteForTesting();
// Set an arbitrarily long timeout to ensure the subframe unload timer doesn't
// fire before OnDetach() is called.
subframe_rfh->SetSubframeUnloadTimeoutForTesting(base::Seconds(30));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_gurl=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
base::WeakPtr<PageImpl> auction_initator_page =
subframe_rfh->auction_initiator_page();
ASSERT_NE(auction_initator_page, nullptr);
EXPECT_EQ(auction_initator_page.get(), &(subframe_rfh->GetPage()));
// Commit a cross-document navigation in the subframe.
NavigationSimulator::NavigateAndCommitFromDocument(kUrlB, subframe);
// The weak pointer to the auction initiator page should be reset.
ASSERT_NE(subframe_rfh, nullptr);
EXPECT_EQ(subframe_rfh->auction_initiator_page(), nullptr);
}
// The weak pointer to the auction initiator page should not be reset upon a
// same-document navigation.
// TODO(crbug.com/936696): Once RenderDocument is launched, remove this test.
TEST_F(AdAuctionServiceImplTest,
DoNotResetAuctionInitiatorPageOnSameDocumentNavigation) {
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
subframe = NavigationSimulator::NavigateAndCommitFromDocument(
GURL("https://a.test/#frag1"), subframe);
RenderFrameHostImpl* subframe_rfh =
static_cast<RenderFrameHostImpl*>(subframe);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_gurl=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group, subframe);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
base::WeakPtr<PageImpl> auction_initator_page =
subframe_rfh->auction_initiator_page();
ASSERT_NE(auction_initator_page, nullptr);
EXPECT_EQ(auction_initator_page.get(), &(subframe_rfh->GetPage()));
GURL same_document_url = GURL("https://a.test/#frag2");
// Commit a same-document navigation in the subframe.
auto simulator =
NavigationSimulator::CreateRendererInitiated(same_document_url, subframe);
simulator->CommitSameDocument();
// The weak pointer to the auction initiator page should not be reset.
ASSERT_NE(subframe_rfh, nullptr);
ASSERT_NE(subframe_rfh->auction_initiator_page(), nullptr);
EXPECT_EQ(subframe_rfh->auction_initiator_page().get(),
&(subframe_rfh->GetPage()));
EXPECT_EQ(subframe_rfh->auction_initiator_page().get(),
auction_initator_page.get());
}
class AdAuctionServiceImplSharedStorageEnabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplSharedStorageEnabledTest() {
feature_list_.InitAndEnableFeature(blink::features::kSharedStorageAPI);
}
std::u16string SharedStorageGet(const url::Origin& context_origin,
const std::u16string& key) {
storage::SharedStorageManager* shared_storage_manager =
static_cast<StoragePartitionImpl*>(
browser_context()->GetDefaultStoragePartition())
->GetSharedStorageManager();
DCHECK(shared_storage_manager);
base::test::TestFuture<storage::SharedStorageManager::GetResult> future;
shared_storage_manager->Get(context_origin, key, future.GetCallback());
storage::SharedStorageManager::GetResult result = future.Take();
return result.data;
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplSharedStorageEnabledTest, SharedStorageWrite) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
sharedStorage.set('key0', 'value0');
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin(
auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
sharedStorage.append('key0', 'value1');
sharedStorage.set('key1', 'value1');
sharedStorage.set('key4', 'value4');
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
sharedStorage.set('key2', 'value2');
return bid;
}
function reportResult() {
sharedStorage.append('key2', 'value3');
sharedStorage.set('key3', 'value3');
sharedStorage.set('key4', 'value4');
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
NavigateAndCommit(kUrlC);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = kOriginC;
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
NavigateAndCommit(kUrlA);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginC};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, std::nullopt);
// Make sure the shared storage mojom methods are invoked as they use a
// dedicated pipe.
task_environment()->RunUntilIdle();
EXPECT_EQ(SharedStorageGet(kOriginC, u"key0"), u"value0value1");
EXPECT_EQ(SharedStorageGet(kOriginC, u"key1"), u"value1");
EXPECT_EQ(SharedStorageGet(kOriginC, u"key4"), u"value4");
EXPECT_EQ(SharedStorageGet(kOriginA, u"key2"), u"value2value3");
EXPECT_EQ(SharedStorageGet(kOriginA, u"key3"), u"value3");
EXPECT_EQ(SharedStorageGet(kOriginA, u"key4"), u"value4");
}
TEST_F(AdAuctionServiceImplSharedStorageEnabledTest,
ScriptErrorAfterSharedStorageWrite) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
sharedStorage.set('key0', 'value0');
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin(
auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
sharedStorage.set('key1', 'value1');
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
sharedStorage.set('key2', 'value2');
triggerReferenceError
return bid;
}
function reportResult() {
sharedStorage.set('key3', 'value3');
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
NavigateAndCommit(kUrlC);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = kOriginC;
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
NavigateAndCommit(kUrlA);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginC};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
// Make sure the shared storage mojom methods are invoked as they use a
// dedicated pipe.
task_environment()->RunUntilIdle();
// When scoreAd() throws an exception after a
// sharedStorage.set('key2', 'value2'), the write operation should still be
// handled.
EXPECT_EQ(SharedStorageGet(kOriginC, u"key0"), u"value0");
EXPECT_EQ(SharedStorageGet(kOriginC, u"key1"), u"");
EXPECT_EQ(SharedStorageGet(kOriginA, u"key2"), u"value2");
EXPECT_EQ(SharedStorageGet(kOriginA, u"key3"), u"");
}
TEST_F(AdAuctionServiceImplSharedStorageEnabledTest,
SharedStoragePermissionsPolicyDisallowsSellerOrigin) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
sharedStorage.set('key0', 'value0');
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin(
auctionSignals, perBuyerSignals, sellerSignals, browserSignals) {
sharedStorage.set('key1', 'value1');
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
sharedStorage.set('key2', 'value2');
return bid;
}
function reportResult() {
sharedStorage.set('key3', 'value3');
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
// Allow only (bidder) origin A in the permissions policy. The auction with
// seller origin C should fail.
auto simulator =
NavigationSimulator::CreateBrowserInitiated(kUrlA, web_contents());
blink::ParsedPermissionsPolicy policy;
policy.emplace_back(
blink::mojom::PermissionsPolicyFeature::kSharedStorage,
/*allowed_origins=*/
std::vector{*blink::OriginWithPossibleWildcards::FromOrigin(kOriginA)},
/*self_if_matches=*/std::nullopt,
/*matches_all_origins=*/false,
/*matches_opaque_src=*/false);
simulator->SetPermissionsPolicyHeader(std::move(policy));
simulator->Commit();
blink::AuctionConfig auction_config;
auction_config.seller = kOriginC;
auction_config.decision_logic_url = kUrlC.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
// Make sure the shared storage mojom methods are invoked as they use a
// dedicated pipe.
task_environment()->RunUntilIdle();
// Only the sharedStorage.set() from generateBid() was successful.
EXPECT_EQ(SharedStorageGet(kOriginA, u"key0"), u"value0");
EXPECT_EQ(SharedStorageGet(kOriginA, u"key1"), u"");
EXPECT_EQ(SharedStorageGet(kOriginC, u"key2"), u"");
EXPECT_EQ(SharedStorageGet(kOriginC, u"key3"), u"");
}
class AdAuctionServiceImplSharedStorageDisabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplSharedStorageDisabledTest() {
feature_list_.InitAndDisableFeature(blink::features::kSharedStorageAPI);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplSharedStorageDisabledTest, SharedStorageNotDefined) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
sharedStorage.set('key0', 'value0');
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = kOriginA;
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
}
class AdAuctionServiceImplPrivateAggregationEnabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplPrivateAggregationEnabledTest() {
feature_list_.InitAndEnableFeature(blink::features::kPrivateAggregationApi);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationReportsForwarded) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
privateAggregation.contributeToHistogram({bucket: 3n, value: 4});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
class TestPrivateAggregationManagerImpl
: public PrivateAggregationManagerImpl {
public:
TestPrivateAggregationManagerImpl(
std::unique_ptr<PrivateAggregationBudgeter> budgeter,
std::unique_ptr<PrivateAggregationHost> host)
: PrivateAggregationManagerImpl(std::move(budgeter),
std::move(host),
/*storage_partition=*/nullptr) {}
};
base::MockRepeatingCallback<void(
PrivateAggregationHost::ReportRequestGenerator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>,
PrivateAggregationBudgetKey,
PrivateAggregationBudgeter::BudgetDeniedBehavior)>
mock_callback;
auto* storage_partition_impl = static_cast<StoragePartitionImpl*>(
browser_context()->GetDefaultStoragePartition());
storage_partition_impl->OverridePrivateAggregationManagerForTesting(
std::make_unique<TestPrivateAggregationManagerImpl>(
std::make_unique<MockPrivateAggregationBudgeter>(),
std::make_unique<PrivateAggregationHost>(
/*on_report_request_received=*/mock_callback.Get(),
/*browser_context=*/storage_partition_impl->browser_context())));
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
base::RunLoop run_loop;
EXPECT_CALL(mock_callback, Run)
.WillOnce(testing::Invoke(
[&](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
ASSERT_EQ(request.payload_contents().contributions.size(), 2u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 2);
EXPECT_EQ(request.payload_contents().contributions[1].bucket, 3);
EXPECT_EQ(request.payload_contents().contributions[1].value, 4);
EXPECT_EQ(request.shared_info().reporting_origin, kOriginA);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kProtectedAudience);
EXPECT_EQ(budget_key.origin(), kOriginA);
EXPECT_EQ(budget_denied_behavior,
PrivateAggregationBudgeter::BudgetDeniedBehavior::
kDontSendReport);
run_loop.Quit();
}));
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
run_loop.Run();
}
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationPermissionsPolicyDisallowsSellerOrigin) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
return bid;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
// Allow only (bidder) origin A in the permissions policy. The auction with
// seller origin C should fail.
{
auto simulator =
NavigationSimulator::CreateBrowserInitiated(kUrlA, web_contents());
blink::ParsedPermissionsPolicy policy;
policy.emplace_back(
blink::mojom::PermissionsPolicyFeature::kPrivateAggregation,
/*allowed_origins=*/
std::vector{*blink::OriginWithPossibleWildcards::FromOrigin(kOriginA)},
/*self_if_matches=*/std::nullopt,
/*matches_all_origins=*/false,
/*matches_opaque_src=*/false);
simulator->SetPermissionsPolicyHeader(std::move(policy));
simulator->Commit();
blink::AuctionConfig auction_config;
auction_config.seller = kOriginC;
auction_config.decision_logic_url = kUrlC.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
}
// In contrast to the case above, additionally allow origin C in the
// permissions policy. The auction with seller origin C should succeed.
{
auto simulator =
NavigationSimulator::CreateBrowserInitiated(kUrlA, web_contents());
blink::ParsedPermissionsPolicy policy;
policy.emplace_back(
blink::mojom::PermissionsPolicyFeature::kPrivateAggregation,
/*allowed_origins=*/
std::vector{*blink::OriginWithPossibleWildcards::FromOrigin(kOriginA),
*blink::OriginWithPossibleWildcards::FromOrigin(kOriginC)},
/*self_if_matches=*/std::nullopt,
/*matches_all_origins=*/false,
/*matches_opaque_src=*/false);
simulator->SetPermissionsPolicyHeader(std::move(policy));
simulator->Commit();
blink::AuctionConfig auction_config;
auction_config.seller = kOriginC;
auction_config.decision_logic_url = kUrlC.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, std::nullopt);
}
}
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationPermissionsPolicyDisallowsBidderOrigin) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
NavigateAndCommit(kUrlC);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = kOriginC;
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
// Allow only (seller) origin A in the permissions policy. The auction with
// bidder origin C should fail.
{
auto simulator =
NavigationSimulator::CreateBrowserInitiated(kUrlA, web_contents());
blink::ParsedPermissionsPolicy policy;
policy.emplace_back(
blink::mojom::PermissionsPolicyFeature::kPrivateAggregation,
/*allowed_origins=*/
std::vector{*blink::OriginWithPossibleWildcards::FromOrigin(kOriginA)},
/*self_if_matches=*/std::nullopt,
/*matches_all_origins=*/false,
/*matches_opaque_src=*/false);
simulator->SetPermissionsPolicyHeader(std::move(policy));
simulator->Commit();
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginC};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, std::nullopt);
}
// In contrast to the case above, additionally allow origin C in the
// permissions policy. The auction with bidder origin C should succeed.
{
auto simulator =
NavigationSimulator::CreateBrowserInitiated(kUrlA, web_contents());
blink::ParsedPermissionsPolicy policy;
policy.emplace_back(
blink::mojom::PermissionsPolicyFeature::kPrivateAggregation,
/*allowed_origins=*/
std::vector{*blink::OriginWithPossibleWildcards::FromOrigin(kOriginA),
*blink::OriginWithPossibleWildcards::FromOrigin(kOriginC)},
/*self_if_matches=*/std::nullopt,
/*matches_all_origins=*/false,
/*matches_opaque_src=*/false);
simulator->SetPermissionsPolicyHeader(std::move(policy));
simulator->Commit();
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginC};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, std::nullopt);
}
}
class PrivateAggregationUseCounterContentBrowserClient
: public AllowInterestGroupContentBrowserClient {
public:
PrivateAggregationUseCounterContentBrowserClient() = default;
~PrivateAggregationUseCounterContentBrowserClient() override = default;
// ContentBrowserClient:
MOCK_METHOD(void,
LogWebFeatureForCurrentPage,
(content::RenderFrameHost*, blink::mojom::WebFeature),
(override));
};
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationUseCountersLogged) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.contributeToHistogramOnEvent("reserved.win",
{bucket: 1n, value: 2});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
PrivateAggregationUseCounterContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiFledge));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiFledgeExtensions));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiEnableDebugMode))
.Times(0);
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
}
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationExtensionsUseCounterNotLoggedOnContributeToHistogram) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
PrivateAggregationUseCounterContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiFledge));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiFledgeExtensions))
.Times(0);
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiEnableDebugMode))
.Times(0);
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
}
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationEnableDebugModeUseCounterLogged) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.enableDebugMode();
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
PrivateAggregationUseCounterContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiFledge));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiFledgeExtensions))
.Times(0);
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiEnableDebugMode));
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
}
// TODO(crbug.com/1356654): Update when use counter coverage is improved.
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationUseCountersNotLoggedOnFailedInvocation) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.enableDebugMode();
privateAggregation.contributeToHistogram({});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
PrivateAggregationUseCounterContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiAll))
.Times(0);
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiFledge))
.Times(0);
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiFledgeExtensions))
.Times(0);
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiEnableDebugMode))
.Times(0);
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
// There should've been a contributeToHistogram() error.
EXPECT_EQ(auction_result, std::nullopt);
}
// Tests that the use counters are logged only once, even when the API is used
// multiple times (and different functions are used).
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationUseCountersLoggedOnlyOnce) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.contributeToHistogramOnEvent("reserved.win",
{bucket: 1n, value: 2});
privateAggregation.contributeToHistogram({bucket: 3n, value: 4});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
privateAggregation.contributeToHistogram({bucket: 5n, value: 6});
privateAggregation.contributeToHistogramOnEvent("reserved.win",
{bucket: 7n, value: 8});
return bid;
}
)";
PrivateAggregationUseCounterContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiAll));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(), blink::mojom::WebFeature::kPrivateAggregationApiFledge));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
main_rfh(),
blink::mojom::WebFeature::kPrivateAggregationApiFledgeExtensions));
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
}
class AdAuctionServiceImplPrivateAggregationMultiCloudTest
: public AdAuctionServiceImplPrivateAggregationEnabledTest {
public:
AdAuctionServiceImplPrivateAggregationMultiCloudTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/
{blink::features::kPrivateAggregationApiMultipleCloudProviders,
aggregation_service::kAggregationServiceMultipleCloudProviders},
/*disabled_features=*/{});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplPrivateAggregationMultiCloudTest,
PrivateAggregationReportsForwarded) {
// Add a mock to intercept calls to the PrivateAggregationHost.
class MockPrivateAggregationHost : public PrivateAggregationHost {
public:
MockPrivateAggregationHost(
base::RepeatingCallback<void(const std::optional<url::Origin>&,
const url::Origin&)> check_coordinator,
base::RepeatingCallback<void(
ReportRequestGenerator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>,
PrivateAggregationBudgetKey,
PrivateAggregationBudgeter::BudgetDeniedBehavior)>
on_report_request_details_received,
content::BrowserContext* browser_context)
: PrivateAggregationHost(std::move(on_report_request_details_received),
browser_context),
check_coordinator_(std::move(check_coordinator)) {
ON_CALL(*this, BindNewReceiver)
.WillByDefault(
[this](url::Origin worklet_origin, url::Origin top_frame_origin,
PrivateAggregationBudgetKey::Api api_for_budgeting,
std::optional<std::string> context_id,
std::optional<base::TimeDelta> timeout,
std::optional<url::Origin> aggregation_coordinator_origin,
mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>
pending_receiver) -> bool {
check_coordinator_.Run(aggregation_coordinator_origin,
worklet_origin);
return PrivateAggregationHost::BindNewReceiver(
std::move(worklet_origin), std::move(top_frame_origin),
api_for_budgeting, std::move(context_id), timeout,
std::move(aggregation_coordinator_origin),
std::move(pending_receiver));
});
}
~MockPrivateAggregationHost() override = default;
MOCK_METHOD(bool,
BindNewReceiver,
(url::Origin,
url::Origin,
PrivateAggregationBudgetKey::Api,
std::optional<std::string>,
std::optional<base::TimeDelta>,
std::optional<url::Origin>,
mojo::PendingReceiver<blink::mojom::PrivateAggregationHost>),
(override));
private:
base::RepeatingCallback<void(const std::optional<url::Origin>&,
const url::Origin&)>
check_coordinator_;
};
class TestPrivateAggregationManagerImpl
: public PrivateAggregationManagerImpl {
public:
TestPrivateAggregationManagerImpl(
std::unique_ptr<PrivateAggregationBudgeter> budgeter,
std::unique_ptr<PrivateAggregationHost> host)
: PrivateAggregationManagerImpl(std::move(budgeter),
std::move(host),
/*storage_partition=*/nullptr) {}
};
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
privateAggregation.contributeToHistogram({bucket: 3n, value: 4});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
privateAggregation.contributeToHistogram({bucket: 3n, value: 4});
return bid;
}
function reportResult() {}
)";
const url::Origin kAwsAggCoordinator = url::Origin::Create(
GURL(aggregation_service::kDefaultAggregationCoordinatorAwsCloud));
base::RunLoop run_loop;
base::RepeatingCallback<void(const std::optional<url::Origin>&,
const url::Origin&)>
check_coordinator = base::BindLambdaForTesting(
[&](const std::optional<url::Origin>& got_coordinator,
const url::Origin& got_worklet) {
EXPECT_EQ(kAwsAggCoordinator, got_coordinator);
run_loop.Quit();
});
auto* storage_partition_impl = static_cast<StoragePartitionImpl*>(
browser_context()->GetDefaultStoragePartition());
auto mock_private_aggregation_host = std::make_unique<
MockPrivateAggregationHost>(
std::move(check_coordinator),
/*on_report_request_received=*/
base::BindRepeating(
[](PrivateAggregationHost::ReportRequestGenerator generator,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
PrivateAggregationBudgetKey budget_key,
PrivateAggregationBudgeter::BudgetDeniedBehavior
budget_denied_behavior) {
AggregatableReportRequest request =
std::move(generator).Run(contributions);
}),
/*browser_context=*/
storage_partition_impl->browser_context());
MockPrivateAggregationHost* private_aggregation_host =
mock_private_aggregation_host.get();
storage_partition_impl->OverridePrivateAggregationManagerForTesting(
std::make_unique<TestPrivateAggregationManagerImpl>(
std::make_unique<MockPrivateAggregationBudgeter>(),
std::move(mock_private_aggregation_host)));
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
interest_group.aggregation_coordinator_origin = kAwsAggCoordinator;
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.aggregation_coordinator_origin = kAwsAggCoordinator;
EXPECT_CALL(*private_aggregation_host, BindNewReceiver);
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, std::nullopt);
InvokeCallbackForURN(*auction_result);
run_loop.Run();
}
class AdAuctionServiceImplPrivateAggregationDisabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplPrivateAggregationDisabledTest() {
feature_list_.InitAndDisableFeature(
blink::features::kPrivateAggregationApi);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplPrivateAggregationDisabledTest,
PrivateAggregationNotExposed) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
// privateAggregation should cause a ReferenceError.
EXPECT_EQ(auction_result, std::nullopt);
}
TEST_F(AdAuctionServiceImplPrivateAggregationDisabledTest,
PrivateAggregationUseCounterNotLogged) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.contributeToHistogram({bucket: 1n, value: 2});
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
PrivateAggregationUseCounterContentBrowserClient browser_client;
ScopedContentBrowserClientSetting setting(&browser_client);
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.priority = 2;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
testing::_, blink::mojom::WebFeature::kPrivateAggregationApiAll))
.Times(0);
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
testing::_, blink::mojom::WebFeature::kPrivateAggregationApiFledge))
.Times(0);
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
// privateAggregation should cause a ReferenceError.
EXPECT_EQ(auction_result, std::nullopt);
}
class AdAuctionServiceImplUpdateUserBiddingSignalsDisabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplUpdateUserBiddingSignalsDisabledTest() {
feature_list_.InitAndDisableFeature(
features::kEnableUpdatingUserBiddingSignals);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplUpdateUserBiddingSignalsDisabledTest,
DisabledUserBiddingSignalsTest) {
network_responder_->RegisterUpdateResponse(kUpdateUrlPath, R"({
"userBiddingSignals": {"new":10},
"ads": [{
"renderURL": "https://example.com/new_render",
"unsupportedField": "InAd"
}],
"adComponents": [{
"renderURL": "https://example.com/new_component",
"unsupportedField": "InAdComponent"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.update_url = kUpdateUrlA;
interest_group.user_bidding_signals.emplace();
interest_group.user_bidding_signals = "{\"old\":4}";
interest_group.bidding_url = kBiddingLogicUrlA;
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys.emplace();
interest_group.trusted_bidding_signals_keys->push_back("key1");
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
auto groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups->size(), 1u);
const auto& group = groups->GetInterestGroups()[0]->interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url(),
"https://example.com/new_render");
ASSERT_EQ(group.ad_components->size(), 1u);
EXPECT_EQ(group.ad_components.value()[0].render_url(),
"https://example.com/new_component");
EXPECT_EQ(group.user_bidding_signals.value(), "{\"old\":4}");
}
class AdAuctionServiceImplKAnonTest
: public AdAuctionServiceImplTest,
public ::testing::WithParamInterface<
auction_worklet::mojom::KAnonymityBidMode> {
public:
AdAuctionServiceImplKAnonTest() {
std::vector<base::test::FeatureRef> enabled_features;
std::vector<base::test::FeatureRef> disabled_features;
switch (kanon_mode()) {
case auction_worklet::mojom::KAnonymityBidMode::kEnforce:
enabled_features.push_back(blink::features::kFledgeConsiderKAnonymity);
enabled_features.push_back(blink::features::kFledgeEnforceKAnonymity);
break;
case auction_worklet::mojom::KAnonymityBidMode::kSimulate:
enabled_features.push_back(blink::features::kFledgeConsiderKAnonymity);
disabled_features.push_back(blink::features::kFledgeEnforceKAnonymity);
break;
case auction_worklet::mojom::KAnonymityBidMode::kNone:
disabled_features.push_back(blink::features::kFledgeConsiderKAnonymity);
disabled_features.push_back(blink::features::kFledgeEnforceKAnonymity);
break;
}
scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
}
auction_worklet::mojom::KAnonymityBidMode kanon_mode() { return GetParam(); }
protected:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Add an interest group with a non-k-anonymous ad and run an ad auction.
TEST_P(AdAuctionServiceImplKAnonTest, RunAdAuctionNotKAnon) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
switch (kanon_mode()) {
case auction_worklet::mojom::KAnonymityBidMode::kNone:
case auction_worklet::mojom::KAnonymityBidMode::kSimulate: {
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render"));
// Running the auction alone should not result in updating the interest
// group's bid count, previous win list or trigger k-anon joins, no matter
// how much time passes.
task_environment()->RunUntilIdle();
{
std::optional<SingleStorageInterestGroup> storage_interest_group(
GetInterestGroup(interest_group.owner, interest_group.name));
ASSERT_TRUE(storage_interest_group.has_value());
EXPECT_EQ(
0,
storage_interest_group.value()->bidding_browser_signals->bid_count);
EXPECT_EQ(0u, storage_interest_group.value()
->bidding_browser_signals->prev_wins.size());
EXPECT_THAT(GetKAnonJoinedIds(), ::testing::UnorderedElementsAre());
}
// Invoking the URN callback (which is done when the result is loaded in a
// frame) updates those fields.
InvokeCallbackForURN(*auction_result);
{
std::optional<SingleStorageInterestGroup> storage_interest_group(
GetInterestGroup(interest_group.owner, interest_group.name));
ASSERT_TRUE(storage_interest_group.has_value());
EXPECT_EQ(
1,
storage_interest_group.value()->bidding_browser_signals->bid_count);
ASSERT_EQ(1u, storage_interest_group.value()
->bidding_browser_signals->prev_wins.size());
ASSERT_EQ(R"({"renderURL":"https://example.com/render"})",
storage_interest_group.value()
->bidding_browser_signals->prev_wins[0]
->ad_json);
}
EXPECT_THAT(
GetKAnonJoinedIds(),
::testing::UnorderedElementsAre(
KAnonKeyForAdBid(interest_group,
interest_group.ads.value()[0].render_url()),
KAnonKeyForAdNameReporting(interest_group,
interest_group.ads.value()[0])));
break;
}
case auction_worklet::mojom::KAnonymityBidMode::kEnforce: {
// The auction should fail because there were no k-anonymous bids.
// Since the auction failed, everything should update immediately.
EXPECT_FALSE(auction_result);
task_environment()->RunUntilIdle();
EXPECT_THAT(
GetKAnonJoinedIds(),
::testing::UnorderedElementsAre(
KAnonKeyForAdBid(interest_group,
interest_group.ads.value()[0].render_url()),
KAnonKeyForAdNameReporting(interest_group,
interest_group.ads.value()[0])));
break;
}
}
}
INSTANTIATE_TEST_SUITE_P(
/* no label */,
AdAuctionServiceImplKAnonTest,
::testing::Values(auction_worklet::mojom::KAnonymityBidMode::kNone,
auction_worklet::mojom::KAnonymityBidMode::kSimulate,
auction_worklet::mojom::KAnonymityBidMode::kEnforce));
class AdAuctionServiceImplBAndATest : public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplBAndATest() {
feature_list_.InitWithFeaturesAndParameters(
{{blink::features::kFledgeBiddingAndAuctionServer,
{{"FledgeBiddingAndAuctionKeyURL", kKeyUrl.spec()}}},
{blink::features::kFledgeBiddingAndAuctionServerAPI, {}}},
{});
}
void ProvideKeys() {
network_responder_->RegisterUpdateResponse(kBAndAKeyPath,
JSONSerializedKeys());
}
void RegisterDeferredKeys() {
network_responder_->RegisterDeferredUpdateResponse(kBAndAKeyPath);
}
void ProvideDeferredKeys() {
network_responder_->DoDeferredUpdateResponse(kBAndAKeyPath,
JSONSerializedKeys());
}
std::string GetSingleSellerResponse() {
std::string response;
// CBOR response computed using https://cbor.me/
/* Response:
{
"adRenderURL":"https://c.test/ad.html",
"interestGroupName":"cars",
"interestGroupOwner":"https://a.test/",
"biddingGroups": {
"https://a.test/": [0]
},
"winReportingURLs": {
"buyerReportingURLs": {
"reportingURL": "https://d.test/buyerReporting",
"interactionReportingURLs": {
"click": "https://e.test/buyerInteractionReporting"
}
},
"topLevelSellerReportingURLs": {
"reportingURL": "https://d.test/sellerReporting",
"interactionReportingURLs": {
"click": "https://e.test/sellerInteractionReporting"
}
}
}
}
*/
// Converted to base64 with `cat | sed 's/#.*//' | xxd -r -p | gzip |
// base64`
EXPECT_TRUE(base::Base64Decode(
"AgAAAMcfiwgAAAAAAAADhZBBCsIwEEU9hiC61k279wIiFIWKB0iTwQbTJE6mbVx6lAre09"
"JSaErR5Xz+e3zmc2ciBS0Ar2lS5UTW7eOYRwSOYiainApVZFIIqW8HNKV1jRlarG+"
"9FraWOgVrkNpW63FvzMonYJgpHJ1+PVhEbwkBv5SaABknaUJ1A1xJfvfbgYcRf5yB/"
"IqMTaACdQGlfo/aTEa5kPi/"
"ajdZ1QvmZj06VdvpvnpiBQjO0GEQn2sNOP33Fyx+ip+zAQAA",
&response));
return response;
}
std::string GetMultiSellerResponse() {
std::string response;
// CBOR response computed using https://cbor.me/
/* Response:
{
"adRenderURL":"https://c.test/ad.html",
"interestGroupName":"cars",
"interestGroupOwner":"https://a.test/",
"biddingGroups": {
"https://a.test/": [0]
},
"bid": 100,
"bidCurrency":"XAU",
"winReportingURLs": {
"buyerReportingURLs": {
"reportingURL": "https://d.test/buyerReporting",
"interactionReportingURLs": {
"click": "https://e.test/buyerInteractionReporting"
}
},
"componentSellerReportingURLs": {
"reportingURL": "https://d.test/sellerReporting",
"interactionReportingURLs": {
"click": "https://e.test/sellerInteractionReporting"
}
}
},
"topLevelSeller": "https://a.test/",
"adMetadata": "\"foo\""
}
*/
// Converted to base64 with `cat | xxd -r -p | gzip |
// xxd -ps -c0 | sed 's/^/02000000dc/' | xxd -r -p | base64 -w0`
EXPECT_TRUE(base::Base64Decode(
"AgAAAPEfiwgAAAAAAAADhZCxTsMwEED5jA7QoRMszc6GGBBSC1JQJdar76BuHJ85X9p07K"
"eU"
"jb/EahSpDhUdfbr3/HQ/"
"ZmlxhGvAOSkgKNDkg3lSAZbkkWRRzjYr1RDvi8JMlaIWgNOV1q5K5GMjQt7szPvDok5vtP"
"7z"
"SbgJ8cA9BR21v/LKYUYbcm/"
"kHMlwIWytLymwaJKkb+O3LJsdST5zcvJsb3oHdo4caEfWKwkYtZyrD2ScNVV72/"
"N0wj+fgdprw3VgT167+v+qxoOqmBOXs+4GWZ3gXNfXUZV2jld/gZrQgETJxq9b//"
"fcv0dAW536AQAA",
&response));
return response;
}
struct AdAuctionDataAndId {
std::string request;
std::optional<base::Uuid> request_id;
std::string error_message;
};
// Gets auction data in the frame `rfh`. If `rfh` is nullptr, uses the main
// frame. Returns the auction data.
std::optional<AdAuctionDataAndId> GetAdAuctionDataAndFlushForFrame(
url::Origin seller,
RenderFrameHost* rfh = nullptr) {
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
rfh ? rfh : main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
std::optional<AdAuctionDataAndId> output;
interest_service->GetInterestGroupAdAuctionData(
seller,
/*coordinator=*/
url::Origin::Create(
GURL(kDefaultBiddingAndAuctionGCPCoordinatorOrigin)),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
AdAuctionDataAndId data;
data.request = std::string(reinterpret_cast<char*>(result.data()),
result.size());
data.request_id = id;
data.error_message = error_message;
output = data;
run_loop.Quit();
}));
interest_service.FlushForTesting();
run_loop.Run();
return output;
}
// Runs an ad auction using the config specified in `auction_config` in the
// frame `rfh`. Calls the provided `promise_callback` during the auction so
// that test code can resolve any promises. Returns the result of the
// auction, which is either a URL to the winning ad, or std::nullopt if no
// ad won the auction.
std::optional<GURL> RunAdAuctionWithPromiseAndFlushForFrame(
const blink::AuctionConfig& auction_config,
base::OnceCallback<void(mojo::Remote<blink::mojom::AbortableAdAuction>&
runner)> promise_callback,
RenderFrameHost* rfh) {
// Use a new service for each call. Keep the service alive as some calls
// (e.g., sending reports via the URN callback) require it not be deleted.
ad_auction_service_.reset();
mojo::Remote<blink::mojom::AbortableAdAuction> abortable_ad_auction;
AdAuctionServiceImpl::CreateMojoService(
rfh, ad_auction_service_.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
std::optional<blink::FencedFrame::RedactedFencedFrameConfig> maybe_config;
ad_auction_service_->RunAdAuction(
auction_config, abortable_ad_auction.BindNewPipeAndPassReceiver(),
base::BindLambdaForTesting(
[&run_loop, &maybe_config](
bool aborted_by_script,
const std::optional<
blink::FencedFrame::RedactedFencedFrameConfig>& config) {
EXPECT_FALSE(aborted_by_script);
maybe_config = config;
run_loop.Quit();
}));
std::move(promise_callback).Run(abortable_ad_auction);
ad_auction_service_.FlushForTesting();
run_loop.Run();
if (!maybe_config) {
return std::nullopt;
}
CHECK(maybe_config->urn_uuid().has_value());
return maybe_config->urn_uuid();
}
protected:
const GURL kKeyUrl = kUrlA.Resolve(kBAndAKeyPath);
base::test::ScopedFeatureList feature_list_;
};
// Expect bad mojo message if we use an invalid coordinator origin. The
// coordinator origin must be secure.
TEST_F(AdAuctionServiceImplTest, HandlesInvalidCoordinatorOrigin) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
url::Origin bad_coordinator =
url::Origin::Create(GURL("http://insecure.coordinator.test/"));
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
interest_service.set_disconnect_handler(run_loop.QuitClosure());
interest_service->GetInterestGroupAdAuctionData(
/*seller=*/test_origin,
/*coordinator=*/bad_coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
ADD_FAILURE() << "This callback should not be invoked.";
}));
run_loop.Run();
}
// Expect bad mojo message if we use an unsupported coordinator origin. The
// coordinator origin must match one of the ones in our list.
TEST_F(AdAuctionServiceImplTest, HandlesUnsupportedCoordinatorOrigin) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
url::Origin bad_coordinator =
url::Origin::Create(GURL("https://unsupported.coordinator.test/"));
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
interest_service->GetInterestGroupAdAuctionData(
/*seller=*/test_origin,
/*coordinator=*/bad_coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
EXPECT_EQ("Invalid Coordinator", error_message);
run_loop.Quit();
}));
run_loop.Run();
// Keep running callbacks to ensure the failed request is cleaned up
// properly.
task_environment()->RunUntilIdle();
}
// Test that interest_group_manager serialize the blob correctly.
TEST_F(AdAuctionServiceImplTest, SerializesAuctionBlob) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetTrustedBiddingSignalsKeys({{"key1", "key2"}})
.SetUserBiddingSignals("{}")
.SetAds(
{{{GURL("https://c.test/ad.html"), /*metadata=*/"do not send",
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"},
{GURL("https://c.test/ad2.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad3.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "456"}}})
.SetAdComponents(
{{{GURL("https://c.test/ad4.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "789"}}})
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin, "cars"},
R"({"renderURL": "https://c.test/ad.html", "adRenderId": "1234"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin, "cars"}, R"({"renderURL": "https://c.test/ad2.html"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin({test_origin, "cars"},
R"({"renderURL": "corrupt JSON)");
task_environment()->FastForwardBy(base::Seconds(1));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive("00000000-0000-0000-0000-000000000000"),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAARqlZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMDAw"
"MC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOhbmh0dHBzOi8v"
"YS50ZXN0WJUfiwgAAAAAAAAAVYy7DoJAEAB9/"
"RCIr1ZKSwtb73Y3sCh7ZBc0xFhw32L8TiOxsZlmJjO8waFFTNJlBtlqjeJqQnBqFYS6CULS2"
"gCb7U68hruRHrkQd7VXoQQk0C9Kz5iHTtpJ2SjdTiwW4+wc5+"
"OUq8Ay6ql6RmQpfocD9RbxQn3yRaqdke7/"
"Cv94fgB8SY7doAAAAHRlbmFibGVEZWJ1Z1JlcG9ydGluZ/"
"UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAA";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(group_names, testing::ElementsAre(testing::Pair(
test_origin, testing::ElementsAre("cars"))));
}
TEST_F(AdAuctionServiceImplTest, SerializesAuctionBlobWithNoGroups) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive("00000000-0000-0000-0000-000000000000"),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ("", base::Base64Encode(msg));
EXPECT_TRUE(group_names.empty());
}
TEST_F(AdAuctionServiceImplTest, SerializesAuctionBlobWithEmptyGroup) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars").Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive("00000000-0000-0000-0000-000000000000"),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ("", base::Base64Encode(msg));
EXPECT_TRUE(group_names.empty());
}
TEST_F(AdAuctionServiceImplTest, SerializesMultipleOwnersAuctionBlob) {
url::Origin test_origin_a = url::Origin::Create(GURL(kOriginStringA));
url::Origin test_origin_b = url::Origin::Create(GURL(kOriginStringB));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, "cars").Build(),
test_origin_a.GetURL().Resolve("/example.html"));
// fast-forward so second join has different time.
task_environment()->FastForwardBy(base::Seconds(1));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetAds(
{{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"},
{GURL("https://c.test/ad2.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad3.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "456"}}})
.SetAdComponents(
{{{GURL("https://c.test/ad4.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "789"}}})
.Build(),
test_origin_a.GetURL().Resolve("/example.html"));
manager_->RecordInterestGroupWin(
{test_origin_a, "cars"},
R"({"renderURL": "https://c.test/ad.html", "adRenderId": "1234"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin_a, "cars"}, R"({"renderURL": "https://c.test/ad2.html"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, "boats")
.SetAds(
{{{GURL("https://c.test/ad6.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat1"},
{GURL("https://c.test/ad7.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad8.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat2"}}})
.SetAdComponents(
{{{GURL("https://c.test/ad9.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat3"}}})
.SetPriority(1.0)
.Build(),
test_origin_a.GetURL().Resolve("/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_b, "trains")
.SetAds(
{{{GURL("https://b.test/ad6.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Train1"},
{GURL("https://b.test/ad7.html"), /*metadata=*/std::nullopt},
{GURL("https://b.test/ad8.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Train2"}}})
.SetAdComponents(
{{{GURL("https://b.test/ad9.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Train3"}}})
.Build(),
test_origin_b.GetURL().Resolve("/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin_a,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive("00000000-0000-0000-0000-000000000000"),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAAZ2lZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMDAw"
"MC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOibmh0dHBzOi8v"
"YS50ZXN0WJcfiwgAAAAAAAAAdc07DsIwEARQCLlQPvxaOAIFLev1KnFEdiOvSUQHPksOimQK"
"hATNFDPSvDgjWI10EAhFytIy9ERGIGiH0g/CxEGfaazYeJmU/"
"Mk1DFedG09IjPesNc4e5cZh0Q6exrNjfbhOHKdy+WZsUVY11utNMiyC/yJwu9v/A/"
"IfQIyrS8zT6YfKXmqteTfTAAAAbmh0dHBzOi8vYi50ZXN0WHAfiwgAAAAAAAAAJYnRDYMwDA"
"VhJMoIjFCkfofEEEflObJTUP+azsKgFeXrTnf18C7Ydx7VMboLtwC30lxOt+"
"RlzQJCsXrtHpPKbqR3XuCedixKnuDfbZw4DPJCaWJW2h4M+3ASxj+2Pxv+"
"8zJsAAAAdGVuYWJsZURlYnVnUmVwb3J0aW5n9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAA";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(
group_names,
testing::UnorderedElementsAre(
testing::Pair(test_origin_a, testing::ElementsAre("boats", "cars")),
testing::Pair(test_origin_b, testing::ElementsAre("trains"))));
}
TEST_F(AdAuctionServiceImplTest, SerializesAuctionBlobWithoutDebugReporting) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(
blink::features::kBiddingAndScoringDebugReportingAPI);
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/"do not send",
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive("00000000-0000-0000-0000-000000000000"),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAAOSlZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMDAw"
"MC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOhbmh0dHBzOi8v"
"YS50ZXN0WF8fiwgAAAAAAAAAa1ycnJhS3JhiaGRskpKXmJuakpxYVJyXVJRfXpxaFJyZnpeY"
"U7wkvSg1OTUvuZIxIykzxTm/"
"NK+EIaOgKLUsPDOvuCEzKz8zDyzICAC+EO2LTgAAAHRlbmFibGVEZWJ1Z1JlcG9ydGluZ/"
"QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAA";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(group_names, testing::ElementsAre(testing::Pair(
test_origin, testing::ElementsAre("cars"))));
}
TEST_F(AdAuctionServiceImplTest, SerializesAuctionBlobWithDebugToken) {
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
switches::kProtectedAudiencesConsentedDebugToken, "myToken");
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds(
{{{GURL("https://c.test/ad.html"), /*metadata=*/"do not send",
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"},
{GURL("https://c.test/ad2.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad3.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "456"}}})
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive("00000000-0000-0000-0000-000000000000"),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(
"AgAAARmmZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMDAw"
"MC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOhbmh0dHBzOi8v"
"YS50ZXN0WGMfiwgAAAAAAAAAa1ycnJhS3JRiaGRskmxiapaSl5ibmpKcWFScl1SUX16cWhSc"
"mZ6XmFO8JL0oNTk1L7mSMSMpM8U5vzSvhCGjoCi1LDwzr7ghMys/"
"Mw8syAgABWZNKFIAAAB0Y29uc2VudGVkRGVidWdDb25maWeiZXRva2VuZ215VG9rZW5raXND"
"b25zZW50ZWT1dGVuYWJsZURlYnVnUmVwb3J0aW5n9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
base::Base64Encode(msg));
EXPECT_THAT(group_names, testing::ElementsAre(testing::Pair(
test_origin, testing::ElementsAre("cars"))));
}
TEST_F(AdAuctionServiceImplTest, SerializesAuctionBlobWithOmitAds) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
blink::features::kBiddingAndScoringDebugReportingAPI);
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/"do not send",
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetAuctionServerRequestFlags(
{blink::AuctionServerRequestFlagsEnum::kOmitAds})
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin, "cars"},
R"({"renderURL": "https://c.test/ad.html", "adRenderId": "1234"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin, "cars"}, R"({"renderURL": "https://c.test/ad2.html"})");
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive("00000000-0000-0000-0000-000000000000"),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAAOSlZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMDAw"
"MC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOhbmh0dHBzOi8v"
"YS50ZXN0WF8fiwgAAAAAAAAAa1yUkpeYm5qSnFhUnJdUlF9enFoUnJmel5hTvCS9KDU5NS+"
"5kikjKTPFOb80r4Qho6AotSw8M6+"
"4qYkhoYkxxdDI2CQzKz8zDyzNCAAKnN0ZTgAAAHRlbmFibGVEZWJ1Z1JlcG9ydGluZ/"
"UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAA";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(group_names, testing::ElementsAre(testing::Pair(
test_origin, testing::ElementsAre("cars"))));
}
TEST_F(AdAuctionServiceImplTest, SerializesAuctionBlobWithFullAds) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
blink::features::kBiddingAndScoringDebugReportingAPI);
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/"please send",
/*size_group=*/"foo",
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetSizeGroups({{{"foo", {"bar"}}}})
.SetAdSizes(
{{{"bar",
{blink::AdSize(1, blink::AdSize::LengthUnit::kPixels, 1,
blink::AdSize::LengthUnit::kPixels)}}}})
.SetAuctionServerRequestFlags(
{blink::AuctionServerRequestFlagsEnum::kIncludeFullAds})
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin, "cars"},
R"({"renderURL": "https://c.test/ad.html", "adRenderId": "1234"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin, "cars"}, R"({"renderURL": "https://c.test/ad2.html"})");
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive("00000000-0000-0000-0000-000000000000"),
/*config=*/blink::mojom::AuctionDataConfig::New(),
/*callback=*/base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAATalZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMDAw"
"MC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOhbmh0dHBzOi8v"
"YS50ZXN0WLEfiwgAAAAAAAAAhc4xDoJAEEBR8SSeACJa2VoYEyuMsR52RnYRZjc7C0Q7uInK"
"QU1oTLTwAD/v9y8FKP2oawqAEODqKgKhhRCj8cRI/"
"pQdWh2Ck02SqDiQhAQw1qGujJg77bxtnLpYWwJmU7BHXKarNTLUhAq8cO5tJ+"
"SPpmCoZCw8KWJ1m+"
"vc4NY2HGbaeWrPhmUYZo8P3P3A6SQP0fPv3fePKa3hSYveY92wlPcAAAB0ZW5hYmxlRGVidW"
"dSZXBvcnRpbmf1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(group_names, testing::ElementsAre(testing::Pair(
test_origin, testing::ElementsAre("cars"))));
}
TEST_F(AdAuctionServiceImplTest, SerializesAuctionBlobWithPerBuyerConfig) {
url::Origin test_origin_a = url::Origin::Create(GURL(kOriginStringA));
url::Origin test_origin_b = url::Origin::Create(GURL(kOriginStringB));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, "cars").Build(),
test_origin_a.GetURL().Resolve("/example.html"));
// fast-forward so second join has different time.
task_environment()->FastForwardBy(base::Seconds(1));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, "cars")
.SetAds(
{{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"},
{GURL("https://c.test/ad2.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad3.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "456"}}})
.SetAdComponents(
{{{GURL("https://c.test/ad4.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "789"}}})
.Build(),
test_origin_a.GetURL().Resolve("/example.html"));
manager_->RecordInterestGroupWin(
{test_origin_a, "cars"},
R"({"renderURL": "https://c.test/ad.html", "adRenderId": "1234"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin_a, "cars"}, R"({"renderURL": "https://c.test/ad2.html"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_a, "boats")
.SetAds(
{{{GURL("https://c.test/ad6.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat1"},
{GURL("https://c.test/ad7.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad8.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat2"}}})
.SetAdComponents(
{{{GURL("https://c.test/ad9.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat3"}}})
.SetPriority(1.0)
.Build(),
test_origin_a.GetURL().Resolve("/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin_b, "trains")
.SetAds(
{{{GURL("https://b.test/ad6.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Train1"},
{GURL("https://b.test/ad7.html"), /*metadata=*/std::nullopt},
{GURL("https://b.test/ad8.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Train2"}}})
.SetAdComponents(
{{{GURL("https://b.test/ad9.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Train3"}}})
.Build(),
test_origin_b.GetURL().Resolve("/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
// Equal size to both buyers. Each buyer gets one interest group.
{
blink::mojom::AuctionDataConfigPtr config =
blink::mojom::AuctionDataConfig::New();
// All groups require 418 bytes, so less than that (plus framing overhead).
config->request_size = 412 + 56;
config->per_buyer_configs.emplace(
test_origin_a, blink::mojom::AuctionDataBuyerConfig::New(/*size=*/256));
config->per_buyer_configs.emplace(
test_origin_b, blink::mojom::AuctionDataBuyerConfig::New(/*size=*/256));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin_a,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive(
"00000000-0000-0000-0000-000000000000"),
/*config=*/std::move(config),
/*callback=*/
base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAAXalZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMD"
"AwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOibmh0dHBz"
"Oi8vYS50ZXN0WHAfiwgAAAAAAAAAHckxDoMwDEZhyo1obwBHYOjsJFYwKr+"
"jOG3FRjgLB0XK8obv1ctTsJNHpTK0PgNoY3ZKxVavW1IwitU2X3BZ/"
"8Z5lgj62BUze4bf+8VJmPSL0i0p8+"
"8tsENWFTR83JFlbjNoAAAAbmh0dHBzOi8vYi50ZXN0WHAfiwgAAAAAAAAAJYnRDYMwDAVh"
"JMoIjFCkfofEEEflObJTUP+azsKgFeXrTnf18C7Ydx7VMboLtwC30lxOt+"
"RlzQJCsXrtHpPKbqR3XuCedixKnuDfbZw4DPJCaWJW2h4M+3ASxj+2Pxv+"
"8zJsAAAAdGVuYWJsZURlYnVnUmVwb3J0aW5n9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAA==";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(
group_names,
testing::UnorderedElementsAre(
testing::Pair(test_origin_a, testing::ElementsAre("boats")),
testing::Pair(test_origin_b, testing::ElementsAre("trains"))));
}
// More size to buyer a, so they get two groups and buyer b gets 0.
{
blink::mojom::AuctionDataConfigPtr config =
blink::mojom::AuctionDataConfig::New();
// All groups require 418 bytes, so less than that.
config->request_size = 412 + 56;
config->per_buyer_configs.emplace(
test_origin_a, blink::mojom::AuctionDataBuyerConfig::New(/*size=*/256));
config->per_buyer_configs.emplace(
test_origin_b, blink::mojom::AuctionDataBuyerConfig::New(/*size=*/128));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin_a,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive(
"00000000-0000-0000-0000-000000000000"),
/*config=*/std::move(config),
/*callback=*/
base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAARylZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMD"
"AwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOhbmh0dHBz"
"Oi8vYS50ZXN0WJcfiwgAAAAAAAAAdc07DsIwEARQCLlQPvxaOAIFLev1KnFEdiOvSUQHPk"
"sOimQKhATNFDPSvDgjWI10EAhFytIy9ERGIGiH0g/CxEGfaazYeJmU/"
"Mk1DFedG09IjPesNc4e5cZh0Q6exrNjfbhOHKdy+WZsUVY11utNMiyC/yJwu9v/A/"
"IfQIyrS8zT6YfKXmqteTfTAAAAdGVuYWJsZURlYnVnUmVwb3J0aW5n9QAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAA==";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(group_names,
testing::UnorderedElementsAre(testing::Pair(
test_origin_a, testing::ElementsAre("boats", "cars"))));
}
// Don't specify size for buyer a, should see 1 group for each owner.
{
blink::mojom::AuctionDataConfigPtr config =
blink::mojom::AuctionDataConfig::New();
// All groups require 418 bytes, so less than that.
config->request_size = 412 + 56;
config->per_buyer_configs.emplace(
test_origin_a, blink::mojom::AuctionDataBuyerConfig::New());
config->per_buyer_configs.emplace(
test_origin_b, blink::mojom::AuctionDataBuyerConfig::New(/*size=*/128));
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin_a,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive(
"00000000-0000-0000-0000-000000000000"),
/*config=*/std::move(config),
/*callback=*/
base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAAXalZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMD"
"AwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOibmh0dHBz"
"Oi8vYS50ZXN0WHAfiwgAAAAAAAAAHckxDoMwDEZhyo1obwBHYOjsJFYwKr+"
"jOG3FRjgLB0XK8obv1ctTsJNHpTK0PgNoY3ZKxVavW1IwitU2X3BZ/"
"8Z5lgj62BUze4bf+8VJmPSL0i0p8+"
"8tsENWFTR83JFlbjNoAAAAbmh0dHBzOi8vYi50ZXN0WHAfiwgAAAAAAAAAJYnRDYMwDAVh"
"JMoIjFCkfofEEEflObJTUP+azsKgFeXrTnf18C7Ydx7VMboLtwC30lxOt+"
"RlzQJCsXrtHpPKbqR3XuCedixKnuDfbZw4DPJCaWJW2h4M+3ASxj+2Pxv+"
"8zJsAAAAdGVuYWJsZURlYnVnUmVwb3J0aW5n9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAA==";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(
group_names,
testing::UnorderedElementsAre(
testing::Pair(test_origin_a, testing::ElementsAre("boats")),
testing::Pair(test_origin_b, testing::ElementsAre("trains"))));
}
// Don't specify size for buyer b, should see only buyer a groups.
{
blink::mojom::AuctionDataConfigPtr config =
blink::mojom::AuctionDataConfig::New();
// All groups require 418 bytes, so less than that.
config->request_size = 412 + 56;
config->per_buyer_configs.emplace(
test_origin_a, blink::mojom::AuctionDataBuyerConfig::New(/*size=*/512));
config->per_buyer_configs.emplace(
test_origin_b, blink::mojom::AuctionDataBuyerConfig::New());
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin_a,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive(
"00000000-0000-0000-0000-000000000000"),
/*config=*/std::move(config),
/*callback=*/
base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAARylZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMD"
"AwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOhbmh0dHBz"
"Oi8vYS50ZXN0WJcfiwgAAAAAAAAAdc07DsIwEARQCLlQPvxaOAIFLev1KnFEdiOvSUQHPk"
"sOimQKhATNFDPSvDgjWI10EAhFytIy9ERGIGiH0g/CxEGfaazYeJmU/"
"Mk1DFedG09IjPesNc4e5cZh0Q6exrNjfbhOHKdy+WZsUVY11utNMiyC/yJwu9v/A/"
"IfQIyrS8zT6YfKXmqteTfTAAAAdGVuYWJsZURlYnVnUmVwb3J0aW5n9QAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAA==";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(group_names,
testing::UnorderedElementsAre(testing::Pair(
test_origin_a, testing::ElementsAre("boats", "cars"))));
}
// Don't specify buyer b at all, should see only buyer a groups.
{
blink::mojom::AuctionDataConfigPtr config =
blink::mojom::AuctionDataConfig::New();
config->request_size = 1024;
config->per_buyer_configs.emplace(
test_origin_a, blink::mojom::AuctionDataBuyerConfig::New());
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin_a,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive(
"00000000-0000-0000-0000-000000000000"),
/*config=*/std::move(config),
/*callback=*/
base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAARylZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMD"
"AwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOhbmh0dHBz"
"Oi8vYS50ZXN0WJcfiwgAAAAAAAAAdc07DsIwEARQCLlQPvxaOAIFLev1KnFEdiOvSUQHPk"
"sOimQKhATNFDPSvDgjWI10EAhFytIy9ERGIGiH0g/CxEGfaazYeJmU/"
"Mk1DFedG09IjPesNc4e5cZh0Q6exrNjfbhOHKdy+WZsUVY11utNMiyC/yJwu9v/A/"
"IfQIyrS8zT6YfKXmqteTfTAAAAdGVuYWJsZURlYnVnUmVwb3J0aW5n9QAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(group_names,
testing::UnorderedElementsAre(testing::Pair(
test_origin_a, testing::ElementsAre("boats", "cars"))));
}
// Don't specify buyers - specify size. We should get an exact size.
// Try something small enough we only can fit 1 interest group each.
{
blink::mojom::AuctionDataConfigPtr config =
blink::mojom::AuctionDataConfig::New();
config->request_size = 381 + 56;
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin_a,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive(
"00000000-0000-0000-0000-000000000000"),
/*config=*/std::move(config),
/*callback=*/
base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
std::string expected =
"AgAAAXalZ3ZlcnNpb24AaXB1Ymxpc2hlcmZhLnRlc3RsZ2VuZXJhdGlvbklkeCQwMDAwMD"
"AwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDBuaW50ZXJlc3RHcm91cHOibmh0dHBz"
"Oi8vYS50ZXN0WHAfiwgAAAAAAAAAHckxDoMwDEZhyo1obwBHYOjsJFYwKr+"
"jOG3FRjgLB0XK8obv1ctTsJNHpTK0PgNoY3ZKxVavW1IwitU2X3BZ/"
"8Z5lgj62BUze4bf+8VJmPSL0i0p8+"
"8tsENWFTR83JFlbjNoAAAAbmh0dHBzOi8vYi50ZXN0WHAfiwgAAAAAAAAAJYnRDYMwDAVh"
"JMoIjFCkfofEEEflObJTUP+azsKgFeXrTnf18C7Ydx7VMboLtwC30lxOt+"
"RlzQJCsXrtHpPKbqR3XuCedixKnuDfbZw4DPJCaWJW2h4M+3ASxj+2Pxv+"
"8zJsAAAAdGVuYWJsZURlYnVnUmVwb3J0aW5n9QAA";
EXPECT_EQ(expected, base::Base64Encode(msg));
EXPECT_THAT(
group_names,
testing::UnorderedElementsAre(
testing::Pair(test_origin_a, testing::ElementsAre("boats")),
testing::Pair(test_origin_b, testing::ElementsAre("trains"))));
}
// Don't specify buyers - specify size. We should get an exact size.
// Try something too small for anything.
{
blink::mojom::AuctionDataConfigPtr config =
blink::mojom::AuctionDataConfig::New();
config->request_size = 20 + 56;
std::vector<uint8_t> msg;
base::flat_map<url::Origin, std::vector<std::string>> group_names;
base::RunLoop run_loop;
manager_->GetInterestGroupAdAuctionData(
/*top_level_origin=*/test_origin_a,
/*generation_id=*/
base::Uuid::ParseCaseInsensitive(
"00000000-0000-0000-0000-000000000000"),
/*config=*/std::move(config),
/*callback=*/
base::BindLambdaForTesting([&](BiddingAndAuctionData data) {
msg = std::move(data.request);
group_names = std::move(data.group_names);
run_loop.Quit();
}));
run_loop.Run();
EXPECT_EQ(0u, msg.size());
EXPECT_THAT(group_names, testing::UnorderedElementsAre());
}
}
TEST_F(AdAuctionServiceImplBAndATest, JoinInterestGroupPrefetchesKeys) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(features::kFledgePrefetchBandAKeys);
RegisterDeferredKeys();
blink::InterestGroup interest_group = CreateInterestGroup();
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
// A first JoinInterestGroup should cause the key to be fetched.
{
base::RunLoop run_loop;
interest_service->JoinInterestGroup(
interest_group,
base::BindLambdaForTesting(
[&](bool failed_well_known_check) { run_loop.Quit(); }));
task_environment()->RunUntilIdle();
ASSERT_TRUE(network_responder_->HasPendingResponse(kBAndAKeyPath));
ProvideDeferredKeys();
interest_service.FlushForTesting();
run_loop.Run();
}
// Now that the key has been fetched, another call to JoinInterestGroup
// won't fetch it again.
RegisterDeferredKeys();
{
base::RunLoop run_loop;
interest_service->JoinInterestGroup(
interest_group,
base::BindLambdaForTesting(
[&](bool failed_well_known_check) { run_loop.Quit(); }));
task_environment()->RunUntilIdle();
ASSERT_FALSE(network_responder_->HasPendingResponse(kBAndAKeyPath));
}
}
TEST_F(AdAuctionServiceImplBAndATest, EncryptsPayload) {
ProvideKeys();
NavigateAndCommit(kUrlA);
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds(
{{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"},
{GURL("https://c.test/ad2.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad3.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "456"}}})
.SetAdComponents(
{{{GURL("https://c.test/ad4.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "789"}}})
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin, "cars"},
R"({"renderURL": "https://c.test/ad.html", "adRenderId": "1234"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->RecordInterestGroupWin(
{test_origin, "cars"}, R"({"renderURL": "https://c.test/ad2.html"})");
task_environment()->FastForwardBy(base::Seconds(1));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "boats")
.SetAds(
{{{GURL("https://c.test/ad6.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat1"},
{GURL("https://c.test/ad7.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad8.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat2"}}})
.SetAdComponents(
{{{GURL("https://c.test/ad9.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "Boat3"}}})
.SetPriority(1.0) // Set a higher priority so this one is first in
// the request.
.Build(),
test_origin.GetURL().Resolve("/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> result =
GetAdAuctionDataAndFlushForFrame(test_origin);
ASSERT_TRUE(result.has_value());
ASSERT_LT(0u, result.value().request.size());
// The message should be a power of 2 in length
EXPECT_EQ(1, absl::popcount(result->request.size()));
auto key_config = quiche::ObliviousHttpHeaderKeyConfig::Create(
0x12, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM)
.value();
auto ohttp_gateway =
quiche::ObliviousHttpGateway::Create(
std::string(reinterpret_cast<const char*>(&kTestPrivateKey[0]),
sizeof(kTestPrivateKey)),
key_config)
.value();
EXPECT_EQ(0x00, result->request[0]);
auto request = ohttp_gateway.DecryptObliviousHttpRequest(
result->request.substr(1), kBiddingAndAuctionEncryptionRequestMediaType);
ASSERT_TRUE(request.ok()) << request.status();
auto plaintext_data = request->GetPlaintextData();
EXPECT_EQ(0x02, plaintext_data[0]);
size_t request_size = 0;
for (size_t idx = 0; idx < sizeof(uint32_t); idx++) {
request_size =
(request_size << 8) | static_cast<uint8_t>(plaintext_data[idx + 1]);
}
// The generation ID is random, so match against everything before and
// everything after.
std::string got_str = cbor::DiagnosticWriter::Write(
cbor::Reader::Read(base::as_bytes(base::make_span(
plaintext_data.substr(5, request_size))))
.value());
EXPECT_THAT(got_str,
testing::StartsWith(R"({"version": 0, "publisher": "a.test", )"
R"("generationId": ")"));
EXPECT_THAT(
got_str,
testing::EndsWith(
R"(", )"
R"("interestGroups": {"https://a.test": )"
R"(h'1F8B080000000000000075CD3B0EC230100450025C281F7E2D3902455AD6EB)"
R"(55E288EC465E1344073E4B0E8A641A90A09962469A176704AB918E02214F5958)"
R"(8681C80804ED5186519838E8338D251B2F37257F722DC345E7D61312E33DEB8C)"
R"(B3B55C392CBAD1D3D438D687EBC5712AB33763F3A2ACB0DA6C936111FC1781BB)"
R"(FDE11FB0FE01C4B83CC7553AFDA05EE2EDA5E5D3000000'}, )"
R"("enableDebugReporting": true})"));
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(result.value().request_id);
AdAuctionRequestContext* context =
page_data->GetContextForAdAuctionRequest(*result.value().request_id);
ASSERT_TRUE(context);
EXPECT_EQ(test_origin, context->seller);
EXPECT_THAT(context->group_names,
testing::UnorderedElementsAre(testing::Pair(
test_origin, testing::ElementsAre("boats", "cars"))));
}
TEST_F(AdAuctionServiceImplBAndATest, OriginNotAllowed) {
base::HistogramTester hist;
ProvideKeys();
content_browser_client_.SetAllowList({kOriginA});
url::Origin test_origin =
url::Origin::Create(GURL("https://not.attested.test/"));
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds(
{{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"},
{GURL("https://c.test/ad2.html"), /*metadata=*/std::nullopt},
{GURL("https://c.test/ad3.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "456"}}})
.SetAdComponents(
{{{GURL("https://c.test/ad4.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "789"}}})
.Build(),
GURL("https://a.test/example.html"));
std::optional<AdAuctionDataAndId> result =
GetAdAuctionDataAndFlushForFrame(test_origin);
EXPECT_TRUE(result.has_value());
EXPECT_EQ("", result.value().request);
EXPECT_EQ("API not allowed for this origin", result.value().error_message);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataSize", 0);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 0);
}
TEST_F(AdAuctionServiceImplBAndATest, RunBAndAAuction) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetSingleSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 2u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre())));
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectUniqueSample("Ads.InterestGroup.ServerAuction.Request.NumGroups",
1, 1);
hist.ExpectUniqueSample(
"Ads.InterestGroup.ServerAuction.Request.RelativeCompressedSize", 122, 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
0);
hist.ExpectUniqueSample("Ads.InterestGroup.ServerAuction.Result",
AuctionResult::kSuccess, 1);
hist.ExpectUniqueSample(
"Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon", true, 1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
1);
hist.ExpectUniqueSample("Ads.InterestGroup.ServerAuction.KeyFetch.Cached",
false, 1);
hist.ExpectUniqueSample(
"Ads.InterestGroup.ServerAuction.KeyFetch.NetworkCached", false, 1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.KeyFetch.NetworkTime",
1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.KeyFetch.TotalTime",
1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.ReportDelay", 1);
// There should be no on-device metrics
hist.ExpectTotalCount("Ads.InterestGroup.Auction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.EndToEndTimeNoWinner", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.Result", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.AuctionWithWinnerTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.ReportDelay", 0);
}
TEST_F(AdAuctionServiceImplBAndATest, RunBAndAAuctionNoBids) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response;
// CBOR response computed using https://cbor.me/
/* Response:
{
"isChaff": true
}
*/
// Converted to base64 with `cat | sed 's/#.*//' | xxd -r -p | gzip | xxd -ps
// -c 0 | sed 's/^/0200000022/' | xxd -r -p | base64`
ASSERT_TRUE(base::Base64Decode(
"AgAAAB4fiwgAAAAAAAADW5ieWeyckZiW9hUA2j0IngoAAAA=", &response));
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_FALSE(result);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
1);
hist.ExpectUniqueSample("Ads.InterestGroup.ServerAuction.Result",
AuctionResult::kNoBids, 1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
0);
// There should be no on-device metrics
hist.ExpectTotalCount("Ads.InterestGroup.Auction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.EndToEndTimeNoWinner", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.Result", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.AuctionWithWinnerTime", 0);
}
TEST_F(AdAuctionServiceImplBAndATest, RunBAndAAuctionServerError) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response;
// CBOR response computed using https://cbor.me/
/* Response:
{"error": {"code": 1, "message": "foo"}}
*/
// Converted to base64 with `cat | sed 's/#.*//' | xxd -r -p | gzip | xxd -ps
// -c 0 | sed 's/^/020000002e/' | xxd -r -p | base64`
ASSERT_TRUE(base::Base64Decode(
"AgAAAC4fiwgAAAAAAAADW5iaWlSUX7QoJTk/JZUxPTe1uDgxPTU5LT8fACX9unIaAAAA",
&response));
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_FALSE(result);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
1);
hist.ExpectUniqueSample("Ads.InterestGroup.ServerAuction.Result",
AuctionResult::kNoBids, 1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
0);
// There should be no on-device metrics
hist.ExpectTotalCount("Ads.InterestGroup.Auction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.EndToEndTimeNoWinner", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.Result", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", 0);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.AuctionWithWinnerTime", 0);
}
TEST_F(AdAuctionServiceImplBAndATest, RunBAndAAuctionWithoutCustomMediaType) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(kBiddingAndAuctionEncryptionMediaType);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
auto key_config = quiche::ObliviousHttpHeaderKeyConfig::Create(
0x12, EVP_HPKE_DHKEM_X25519_HKDF_SHA256,
EVP_HPKE_HKDF_SHA256, EVP_HPKE_AES_256_GCM)
.value();
auto ohttp_gateway =
quiche::ObliviousHttpGateway::Create(
std::string(reinterpret_cast<const char*>(&kTestPrivateKey[0]),
sizeof(kTestPrivateKey)),
key_config)
.value();
auto request = ohttp_gateway.DecryptObliviousHttpRequest(
auction_data->request,
quiche::ObliviousHttpHeaderKeyConfig::kOhttpRequestLabel);
EXPECT_TRUE(request.ok()) << request.status();
auto plaintext_data = request->GetPlaintextData();
// The message should be a power of 2 in length
EXPECT_EQ(1, absl::popcount(auction_data->request.size()));
EXPECT_EQ(0x02, plaintext_data[0]);
size_t request_size = 0;
for (size_t idx = 0; idx < sizeof(uint32_t); idx++) {
request_size =
(request_size << 8) | static_cast<uint8_t>(plaintext_data[idx + 1]);
}
// The generation ID is random, so match against everything before and
// everything after.
std::string got_str = cbor::DiagnosticWriter::Write(
cbor::Reader::Read(base::as_bytes(base::make_span(
plaintext_data.substr(5, request_size))))
.value());
EXPECT_THAT(got_str,
testing::StartsWith(R"({"version": 0, "publisher": "a.test", )"
R"("generationId": ")"));
EXPECT_THAT(
got_str,
testing::EndsWith(
R"(", )"
R"("interestGroups": {"https://a.test": )"
R"(h'1F8B08000000000000006B5C9C9C9852DC986268646C929297989B9A929C58)"
R"(549C9754945F5E9C5A149C999E979853BC24BD283539352FB99231232933C539)"
R"(BF34AF8421A3A028B52C3C33AFB821332B3F330F2CC80800BE10ED8B4E000000)"
R"('}, "enableDebugReporting": true})"));
std::string response = GetSingleSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
std::string encrypted_response =
ohttp_gateway
.CreateObliviousHttpResponse(
response, request_context->context,
quiche::ObliviousHttpHeaderKeyConfig::kOhttpResponseLabel)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 2u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre())));
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 1);
}
TEST_F(AdAuctionServiceImplBAndATest, HandlesBadResponseForBAndAAuction) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = "Invalid Response: Is not CBOR!";
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_FALSE(result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 0u);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
0);
hist.ExpectUniqueSample("Ads.InterestGroup.ServerAuction.Result",
AuctionResult::kInvalidServerResponse, 1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
0);
}
TEST_F(AdAuctionServiceImplBAndATest,
RunMultiSellerBAndAAuctionInSingleSeller) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
// A component auction response cannot be used for a regular auction.
EXPECT_FALSE(result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 0u);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
0);
hist.ExpectUniqueSample("Ads.InterestGroup.ServerAuction.Result",
AuctionResult::kInvalidServerResponse, 1);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
0);
}
TEST_F(AdAuctionServiceImplBAndATest, RunBAndAAuctionAsMultiseller) {
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetSingleSellerResponse();
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
// A regular response can't be used for a component auction.
EXPECT_FALSE(result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 0u);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
0);
}
TEST_F(AdAuctionServiceImplBAndATest, RunMultiSellerBAndAAuctionWrongSeller) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginB;
auction_config.decision_logic_url = kUrlB.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
// A component auction response cannot be used if the top level seller doesn't
// match.
EXPECT_FALSE(result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 0u);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
0);
}
TEST_F(AdAuctionServiceImplBAndATest, RunMultiSellerBAndAAuction) {
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
if (adMetadata !== "foo") {
throw new Error('Bad metadata');
}
if (browserSignals.bidCurrency != "XAU") {
throw new Error('Bad currency');
}
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
ASSERT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/topLevelSellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre())));
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
}
TEST_F(AdAuctionServiceImplBAndATest,
RunMultiSellerBAndAAuctionWithOtherPromisesResolveLater) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {
'ad': 'example',
'bid': 1,
'render': 'https://c.test/ad.html',
'allowComponentAuction': true};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
// These signals shouldn't affect the auction, but providing them shouldn't
// break things.
component_auction1.non_shared_params.auction_signals =
blink::AuctionConfig::MaybePromiseJson::FromPromise();
component_auction1.non_shared_params.seller_signals =
blink::AuctionConfig::MaybePromiseJson::FromPromise();
component_auction1.non_shared_params.per_buyer_signals =
blink::AuctionConfig::MaybePromisePerBuyerSignals::FromPromise();
component_auction1.non_shared_params.buyer_timeouts =
blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromPromise();
component_auction1.non_shared_params.buyer_cumulative_timeouts =
blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromPromise();
component_auction1.non_shared_params.buyer_currencies =
blink::AuctionConfig::MaybePromiseBuyerCurrencies::FromPromise();
component_auction1.direct_from_seller_signals =
blink::AuctionConfig::MaybePromiseDirectFromSellerSignals::FromPromise();
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting([&](mojo::Remote<
blink::mojom::AbortableAdAuction>&
runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
// Add an extra delay to ensure that the response was processed first.
task_environment()->RunUntilIdle();
runner->ResolvedPromiseParam(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
blink::mojom::AuctionAdConfigField::kAuctionSignals, "{}");
runner->ResolvedPromiseParam(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
blink::mojom::AuctionAdConfigField::kSellerSignals, "{}");
runner->ResolvedPerBuyerSignalsPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), {});
runner->ResolvedBuyerTimeoutsPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
blink::mojom::AuctionAdConfigBuyerTimeoutField::kPerBuyerTimeouts,
{});
runner->ResolvedBuyerTimeoutsPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
blink::mojom::AuctionAdConfigBuyerTimeoutField::
kPerBuyerCumulativeTimeouts,
{});
runner->ResolvedBuyerCurrenciesPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), {});
runner->ResolvedDirectFromSellerSignalsPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), {});
}),
main_rfh());
EXPECT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/topLevelSellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre())));
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
}
TEST_F(AdAuctionServiceImplBAndATest,
RunMultiSellerBAndAAuctionWithOtherPromisesResolveFirst) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {
'ad': 'example',
'bid': 1,
'render': 'https://c.test/ad.html',
'allowComponentAuction': true};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
// These signals shouldn't affect the auction, but providing them shouldn't
// break things.
component_auction1.non_shared_params.auction_signals =
blink::AuctionConfig::MaybePromiseJson::FromPromise();
component_auction1.non_shared_params.seller_signals =
blink::AuctionConfig::MaybePromiseJson::FromPromise();
component_auction1.non_shared_params.per_buyer_signals =
blink::AuctionConfig::MaybePromisePerBuyerSignals::FromPromise();
component_auction1.non_shared_params.buyer_timeouts =
blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromPromise();
component_auction1.non_shared_params.buyer_cumulative_timeouts =
blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromPromise();
component_auction1.non_shared_params.buyer_currencies =
blink::AuctionConfig::MaybePromiseBuyerCurrencies::FromPromise();
component_auction1.direct_from_seller_signals =
blink::AuctionConfig::MaybePromiseDirectFromSellerSignals::FromPromise();
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting([&](mojo::Remote<
blink::mojom::AbortableAdAuction>&
runner) {
runner->ResolvedPromiseParam(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
blink::mojom::AuctionAdConfigField::kAuctionSignals, "{}");
runner->ResolvedPromiseParam(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
blink::mojom::AuctionAdConfigField::kSellerSignals, "{}");
runner->ResolvedPerBuyerSignalsPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), {});
runner->ResolvedBuyerTimeoutsPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
blink::mojom::AuctionAdConfigBuyerTimeoutField::kPerBuyerTimeouts,
{});
runner->ResolvedBuyerTimeoutsPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
blink::mojom::AuctionAdConfigBuyerTimeoutField::
kPerBuyerCumulativeTimeouts,
{});
runner->ResolvedBuyerCurrenciesPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), {});
runner->ResolvedDirectFromSellerSignalsPromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0), {});
// Resolving these promises allows the auction to start, so add an extra
// delay for these signals to be processed so the auction will move to
// the generate bids phase.
task_environment()->RunUntilIdle();
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/topLevelSellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre())));
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
}
TEST_F(AdAuctionServiceImplBAndATest, RunMultiSellerBAndAAuctionWithLocal) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {
'ad': 'example',
'bid': 1,
'render': 'https://c.test/ad.html',
'allowComponentAuction': true};
}
function reportWin() {}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
blink::AuctionConfig component_auction2;
component_auction2.seller = kOriginA;
component_auction2.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction2.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction2));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/topLevelSellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre())));
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
0);
}
TEST_F(AdAuctionServiceImplBAndATest,
RunMultiSellerBAndAAuctionWithWinningLocal) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {
'ad': 'example',
'bid': 1000,
'render': 'https://c.test/ad.html',
'allowComponentAuction': true};
}
function reportWin() {
sendReportTo('https://d.test/localWinner');
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/localWinner",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
blink::AuctionConfig component_auction2;
component_auction2.seller = kOriginA;
component_auction2.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction2.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction2));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/localWinner"));
EXPECT_TRUE(network_responder_->ReportSent("/topLevelSellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre()),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre()),
testing::Pair(blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre())));
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
hist.ExpectTotalCount(
"Ads.InterestGroup.Auction.ParseBaServerResponseDuration", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTime", 0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.EndToEndTimeNoWinner",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon",
0);
hist.ExpectTotalCount("Ads.InterestGroup.ServerAuction.AuctionWithWinnerTime",
0);
}
TEST_F(AdAuctionServiceImplBAndATest, GetInterestGroupAdAuctionDataNoKeys) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds(
{{{GURL("https://c.test/ad1.html"), /*metadata=*/std::nullopt}}})
.Build(),
GURL("https://a.test/example.html"));
std::optional<AdAuctionDataAndId> output =
GetAdAuctionDataAndFlushForFrame(test_origin);
EXPECT_TRUE(output.has_value());
EXPECT_TRUE(output->request.empty());
EXPECT_FALSE(output->error_message.empty());
}
TEST_F(AdAuctionServiceImplBAndATest,
GetInterestGroupAdAuctionDataNoKeysAndNoInterestGroups) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
std::optional<AdAuctionDataAndId> output =
GetAdAuctionDataAndFlushForFrame(test_origin);
EXPECT_TRUE(output.has_value());
EXPECT_TRUE(output->request.empty());
EXPECT_FALSE(output->error_message.empty());
}
TEST_F(AdAuctionServiceImplBAndATest,
GetInterestGroupAdAuctionDataKeysAndNoInterestGroups) {
ProvideKeys();
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
std::optional<AdAuctionDataAndId> output =
GetAdAuctionDataAndFlushForFrame(test_origin);
EXPECT_TRUE(output.has_value());
EXPECT_TRUE(output->request.empty());
EXPECT_TRUE(output->error_message.empty());
}
TEST_F(AdAuctionServiceImplBAndATest,
GetInterestGroupAdAuctionData_KeysLoadBeforeIGs) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
// If we register the response, it will be returned right away, before the
// interest groups get a chance to load.
ProvideKeys();
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds(
{{{GURL("https://c.test/ad1.html"), /*metadata=*/std::nullopt}}})
.Build(),
GURL("https://a.test/example.html"));
std::optional<AdAuctionDataAndId> output =
GetAdAuctionDataAndFlushForFrame(test_origin);
EXPECT_TRUE(output.has_value());
EXPECT_FALSE(output->request.empty());
EXPECT_TRUE(output->error_message.empty());
}
TEST_F(AdAuctionServiceImplBAndATest,
GetInterestGroupAdAuctionData_IGsLoadBeforeKeys) {
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
RegisterDeferredKeys();
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds(
{{{GURL("https://c.test/ad1.html"), /*metadata=*/std::nullopt}}})
.Build(),
GURL("https://a.test/example.html"));
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
url::Origin coordinator =
url::Origin::Create(GURL(kDefaultBiddingAndAuctionGCPCoordinatorOrigin));
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
std::optional<AdAuctionDataAndId> output;
interest_service->GetInterestGroupAdAuctionData(
test_origin, coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
AdAuctionDataAndId data;
data.request =
std::string(reinterpret_cast<char*>(result.data()), result.size());
data.request_id = id;
data.error_message = error_message;
output = data;
run_loop.Quit();
}));
task_environment()->RunUntilIdle();
ASSERT_TRUE(network_responder_->HasPendingResponse(kBAndAKeyPath));
ProvideDeferredKeys();
interest_service.FlushForTesting();
run_loop.Run();
EXPECT_TRUE(output.has_value());
EXPECT_FALSE(output->request.empty());
EXPECT_TRUE(output->error_message.empty());
}
TEST_F(AdAuctionServiceImplBAndATest,
HandlesMultipleGetInterestGroupAdAuctionDataInARow) {
ProvideKeys();
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "cars")
.SetAds(
{{{GURL("https://c.test/ad1.html"), /*metadata=*/std::nullopt}}})
.Build(),
GURL("https://a.test/example.html"));
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(test_origin, "boats")
.SetAds(
{{{GURL("https://c.test/ad2.html"), /*metadata=*/std::nullopt}}})
.Build(),
GURL("https://a.test/example.html"));
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
url::Origin coordinator =
url::Origin::Create(GURL(kDefaultBiddingAndAuctionGCPCoordinatorOrigin));
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
std::optional<AdAuctionDataAndId> output1;
std::optional<AdAuctionDataAndId> output2;
std::optional<AdAuctionDataAndId> output3;
interest_service->GetInterestGroupAdAuctionData(
test_origin, coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
AdAuctionDataAndId data;
data.request =
std::string(reinterpret_cast<char*>(result.data()), result.size());
data.request_id = id;
data.error_message = error_message;
output1 = data;
}));
interest_service->GetInterestGroupAdAuctionData(
test_origin, coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
AdAuctionDataAndId data;
data.request =
std::string(reinterpret_cast<char*>(result.data()), result.size());
data.request_id = id;
data.error_message = error_message;
output2 = data;
}));
interest_service->GetInterestGroupAdAuctionData(
test_origin, coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
AdAuctionDataAndId data;
data.request =
std::string(reinterpret_cast<char*>(result.data()), result.size());
data.request_id = id;
data.error_message = error_message;
output3 = data;
run_loop.Quit();
}));
interest_service.FlushForTesting();
run_loop.Run();
EXPECT_TRUE(output1.has_value());
EXPECT_TRUE(output2.has_value());
EXPECT_TRUE(output3.has_value());
EXPECT_TRUE(output1->error_message.empty());
EXPECT_TRUE(output2->error_message.empty());
EXPECT_TRUE(output3->error_message.empty());
EXPECT_FALSE(output1->request.empty());
EXPECT_FALSE(output2->request.empty());
EXPECT_FALSE(output3->request.empty());
}
TEST_F(AdAuctionServiceImplBAndATest,
HandlesMultipleEmptyGetInterestGroupAdAuctionDataInARow) {
ProvideKeys();
url::Origin test_origin = url::Origin::Create(GURL(kOriginStringA));
mojo::Remote<blink::mojom::AdAuctionService> interest_service;
url::Origin coordinator =
url::Origin::Create(GURL(kDefaultBiddingAndAuctionGCPCoordinatorOrigin));
AdAuctionServiceImpl::CreateMojoService(
main_rfh(), interest_service.BindNewPipeAndPassReceiver());
base::RunLoop run_loop;
std::optional<AdAuctionDataAndId> output1;
std::optional<AdAuctionDataAndId> output2;
std::optional<AdAuctionDataAndId> output3;
interest_service->GetInterestGroupAdAuctionData(
test_origin, coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
AdAuctionDataAndId data;
data.request =
std::string(reinterpret_cast<char*>(result.data()), result.size());
data.request_id = id;
data.error_message = error_message;
output1 = data;
}));
interest_service->GetInterestGroupAdAuctionData(
test_origin, coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
AdAuctionDataAndId data;
data.request =
std::string(reinterpret_cast<char*>(result.data()), result.size());
data.request_id = id;
data.error_message = error_message;
output2 = data;
}));
interest_service->GetInterestGroupAdAuctionData(
test_origin, coordinator,
/*config=*/blink::mojom::AuctionDataConfig::New(),
base::BindLambdaForTesting([&](mojo_base::BigBuffer result,
const std::optional<base::Uuid>& id,
const std::string& error_message) {
AdAuctionDataAndId data;
data.request =
std::string(reinterpret_cast<char*>(result.data()), result.size());
data.request_id = id;
data.error_message = error_message;
output3 = data;
run_loop.Quit();
}));
interest_service.FlushForTesting();
run_loop.Run();
EXPECT_TRUE(output1.has_value());
EXPECT_TRUE(output2.has_value());
EXPECT_TRUE(output3.has_value());
EXPECT_TRUE(output1->error_message.empty());
EXPECT_TRUE(output2->error_message.empty());
EXPECT_TRUE(output3->error_message.empty());
EXPECT_TRUE(output1->request.empty());
EXPECT_TRUE(output2->request.empty());
EXPECT_TRUE(output3->request.empty());
}
TEST_F(AdAuctionServiceImplBAndATest,
RunMultiSellerBAndAAuctionMatchedCurrency) {
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
if (adMetadata !== "foo") {
throw new Error('Bad metadata');
}
if (browserSignals.bidCurrency != "XAU") {
throw new Error('Bad currency');
}
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.buyer_currencies =
blink::AuctionConfig::MaybePromiseBuyerCurrencies::FromValue(
{/*all_buyers_currency=*/blink::AdCurrency::From("XAU"),
/*per_buyers_currencies=*/{}});
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.seller_currency =
blink::AdCurrency::From("XAU");
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
ASSERT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/topLevelSellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre())));
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
}
TEST_F(AdAuctionServiceImplBAndATest,
RunMultiSellerBAndAAuctionMismatchSellerCurrency) {
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
if (adMetadata !== "foo") {
throw new Error('Bad metadata');
}
if (browserSignals.bidCurrency != "XAU") {
throw new Error('Bad currency');
}
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.buyer_currencies =
blink::AuctionConfig::MaybePromiseBuyerCurrencies::FromValue(
{/*all_buyers_currency=*/blink::AdCurrency::From("XAU"),
/*per_buyers_currencies=*/{}});
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.seller_currency =
blink::AdCurrency::From("XPD"); // Mismatch
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
ASSERT_FALSE(result);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
}
TEST_F(AdAuctionServiceImplBAndATest,
RunMultiSellerBAndAAuctionMismatchAllSellerCurrency) {
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
if (adMetadata !== "foo") {
throw new Error('Bad metadata');
}
if (browserSignals.bidCurrency != "XAU") {
throw new Error('Bad currency');
}
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.buyer_currencies =
blink::AuctionConfig::MaybePromiseBuyerCurrencies::FromValue(
{/*all_buyers_currency=*/blink::AdCurrency::From("XAG"),
/*per_buyer_currencies=*/{}}); // Mismatch
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.seller_currency =
blink::AdCurrency::From("XAU");
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
ASSERT_FALSE(result);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
}
TEST_F(AdAuctionServiceImplBAndATest,
RunMultiSellerBAndAAuctionMismatchPerSellerCurrency) {
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
if (adMetadata !== "foo") {
throw new Error('Bad metadata');
}
if (browserSignals.bidCurrency != "XAU") {
throw new Error('Bad currency');
}
return {desirability: 1 + bid, allowComponentAuction: true};
}
function reportResult(auctionConfig, browserSignals) {
sendReportTo('https://d.test/topLevelSellerReporting');
}
)";
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetMultiSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.non_shared_params.buyer_currencies =
blink::AuctionConfig::MaybePromiseBuyerCurrencies::FromValue(
{/*all_buyers_currency=*/blink::AdCurrency::From("XAU"),
/*per_buyer_currencies=*/
{{{kOriginA, blink::AdCurrency::From("XAG")}}}}); // Mismatch
blink::AuctionConfig component_auction1;
component_auction1.seller = kOriginA;
component_auction1.non_shared_params.seller_currency =
blink::AdCurrency::From("XAU");
component_auction1.non_shared_params.interest_group_buyers = {kOriginA};
component_auction1.server_response.emplace();
component_auction1.server_response->request_id = *auction_data->request_id;
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction1));
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewComponentAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
ASSERT_FALSE(result);
// Request should be padded to 512 bytes.
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
}
TEST_F(AdAuctionServiceImplBAndATest, RunServerMultiSellerBAndAAuction) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response;
// CBOR response computed using https://cbor.me/
/* Response:
{
"adRenderURL":"https://c.test/ad.html",
"interestGroupName":"cars",
"interestGroupOwner":"https://a.test/",
"biddingGroups": {
"https://a.test/": [0]
},
"winReportingURLs": {
"buyerReportingURLs": {
"reportingURL": "https://d.test/buyerReporting",
"interactionReportingURLs": {
"click": "https://e.test/buyerInteractionReporting"
}
},
"topLevelSellerReportingURLs": {
"reportingURL": "https://d.test/topLevelSellerReporting",
"interactionReportingURLs": {
"click": "https://e.test/topLevelSellerInteractionReporting"
}
},
"componentSellerReportingURLs": {
"reportingURL": "https://d.test/sellerReporting",
"interactionReportingURLs": {
"click": "https://e.test/sellerInteractionReporting"
}
}
},
"adMetadata": "\"foo\""
}
*/
// Converted to base64 with `cat | xxd -r -p | gzip |
// xxd -ps -c0 | sed 's/^/02000000f5/' | xxd -r -p | base64 -w0`
ASSERT_TRUE(base::Base64Decode(
"AgAAAN8fiwgAAAAAAAADlZJNCsIwEEY9huDPUnTT4tYLiCAKFQ8Qk8EG0yROxrYuPUoV72lR"
"CiZUxeUM3/d4MHM/"
"MJGAFoDbZJmnRNbN4phHBI5iJqKUMpXtpBBS7+doTtZVpkmxV+"
"rSsYXUCViDVKdqjrvh7nQG9HZXhW9jOWgo4kXxC2VXagJknKTx0RVwJfmhHDd9eOsvWkplj4"
"xdQg5qA0p9lxoFUh+av+2mgZ0PatXsc5NZo0HTb89h4On+9ZsEfu6j1/"
"GJqjPP669YBoIzdOit14UGDP/iAbxRR1hbAgAA",
&response));
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/topLevelSellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
ASSERT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 3u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/topLevelSellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre(testing::Pair(
"click",
GURL(
"https://e.test/topLevelSellerInteractionReporting"))))));
const size_t kExpectedBaDataSize = 512;
hist.ExpectUniqueSample("Ads.InterestGroup.BaDataSize", kExpectedBaDataSize,
1);
hist.ExpectTotalCount("Ads.InterestGroup.BaDataConstructionTime", 1);
}
TEST_F(AdAuctionServiceImplBAndATest, RunBAndAAuctionWithBid) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response;
// CBOR response computed using https://cbor.me/
// Includes a bid field that is not required, but is allowed.
/* Response:
{
"adRenderURL":"https://c.test/ad.html",
"interestGroupName":"cars",
"interestGroupOwner":"https://a.test/",
"biddingGroups": {
"https://a.test/": [0]
},
"bid": 1.0,
"winReportingURLs": {
"buyerReportingURLs": {
"reportingURL": "https://d.test/buyerReporting",
"interactionReportingURLs": {
"click": "https://e.test/buyerInteractionReporting"
}
},
"topLevelSellerReportingURLs": {
"reportingURL": "https://d.test/sellerReporting",
"interactionReportingURLs": {
"click": "https://e.test/sellerInteractionReporting"
}
}
}
}
*/
// Converted to base64 with `cat | sed 's/#.*//' | xxd -r -p | gzip |
// base64`
EXPECT_TRUE(base::Base64Decode(
"AgAAAM4fiwgAAAAAAAADhZBBCsIwEEU9hiC61k27F/"
"ciiELFA6TJoME0iZNpG5cepS68oztLS6EpRZfz+e/"
"xmTdPpfhsJjcmEtAC8JzsiyuRdes45hGBo5iJ6EqZyuqmkPqyRZNbV5muxdrWc2JLqROwBql"
"u1R73wjR/AIaZwt7p551FtJYQ8FOpCZBxkiZUV8CV5De/7Hjo8bsRyM/"
"I2D0UoE6g1O9Ri8EoFxL/V60Gq1rB2Kx7o6o7zVcPLAPBGToM4mOpAYf//"
"gI0JYFGugEAAA==",
&response));
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 2u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre())));
hist.ExpectUniqueSample(
"Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon", true, 1);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", 0);
}
class AdAuctionServiceImplBAndAKAnonTest
: public AdAuctionServiceImplBAndATest {
public:
AdAuctionServiceImplBAndAKAnonTest() {
feature_list_.InitWithFeatures(
/*enabled_features=*/{blink::features::kFledgeConsiderKAnonymity,
blink::features::kFledgeEnforceKAnonymity},
/*disabled_features=*/{});
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplBAndAKAnonTest, RunBAndAAuctionWithKAnon) {
base::HistogramTester hist;
ProvideKeys();
NavigateAndCommit(kUrlA);
manager_->JoinInterestGroup(
blink::TestInterestGroupBuilder(kOriginA, "cars")
.SetAds({{{GURL("https://c.test/ad.html"), /*metadata=*/std::nullopt,
/*size_group=*/std::nullopt,
/*buyer_reporting_id=*/std::nullopt,
/*buyer_and_seller_reporting_id=*/std::nullopt, "1234"}}})
.SetBiddingUrl(kBiddingLogicUrlA)
.Build(),
GURL("https://a.test/example.html"));
task_environment()->FastForwardBy(base::Seconds(1));
std::optional<AdAuctionDataAndId> auction_data =
GetAdAuctionDataAndFlushForFrame(kOriginA);
EXPECT_TRUE(auction_data.has_value());
AdAuctionPageData* page_data = PageUserData<AdAuctionPageData>::GetForPage(
static_cast<RenderFrameHostImpl*>(main_rfh())->GetPage());
ASSERT_TRUE(page_data);
ASSERT_TRUE(auction_data->request_id);
AdAuctionRequestContext* request_context =
page_data->GetContextForAdAuctionRequest(*auction_data->request_id);
std::string response = GetSingleSellerResponse();
network_responder_->RegisterReportResponse("/buyerReporting",
/*response=*/"");
network_responder_->RegisterReportResponse("/sellerReporting",
/*response=*/"");
std::string encrypted_response =
quiche::ObliviousHttpResponse::CreateServerObliviousResponse(
response, request_context->context,
kBiddingAndAuctionEncryptionResponseMediaType)
->EncapsulateAndSerialize();
page_data->AddAuctionResultWitnessForOrigin(
kOriginA, crypto::SHA256HashString(encrypted_response));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
auction_config.server_response.emplace();
auction_config.server_response->request_id = *auction_data->request_id;
std::optional<GURL> result = RunAdAuctionWithPromiseAndFlushForFrame(
auction_config,
base::BindLambdaForTesting(
[&](mojo::Remote<blink::mojom::AbortableAdAuction>& runner) {
runner->ResolvedAuctionAdResponsePromise(
blink::mojom::AuctionAdConfigAuctionId::NewMainAuction(0),
mojo_base::BigBuffer(
base::as_bytes(base::make_span(encrypted_response))));
}),
main_rfh());
EXPECT_TRUE(result);
InvokeCallbackForURN(*result);
// Fast forward enough for all reports to be sent.
task_environment()->FastForwardBy(base::Hours(1));
EXPECT_EQ(network_responder_->ReportCount(), 2u);
EXPECT_TRUE(network_responder_->ReportSent("/buyerReporting"));
EXPECT_TRUE(network_responder_->ReportSent("/sellerReporting"));
std::optional<FencedFrameProperties> properties =
GetFencedFramePropertiesForURN(*result);
ASSERT_TRUE(properties);
EXPECT_THAT(
properties->fenced_frame_reporter()->GetAdBeaconMapForTesting(),
testing::UnorderedElementsAre(
testing::Pair(
blink::FencedFrame::ReportingDestination::kBuyer,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/buyerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kSeller,
testing::ElementsAre(testing::Pair(
"click", GURL("https://e.test/sellerInteractionReporting")))),
testing::Pair(
blink::FencedFrame::ReportingDestination::kComponentSeller,
testing::ElementsAre())));
hist.ExpectUniqueSample(
"Ads.InterestGroup.ServerAuction.NonKAnonWinnerIsKAnon", true, 1);
hist.ExpectTotalCount("Ads.InterestGroup.Auction.NonKAnonWinnerIsKAnon", 0);
}
class AdAuctionServiceImplFacilitatedTestingTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplFacilitatedTestingTest() {
features_.InitWithFeaturesAndParameters(
{{features::kCookieDeprecationFacilitatedTesting,
{{"label", "LabelForTesting"}}},
{features::kFledgeFacilitatedTestingSignalsHeaders, {}}},
{});
}
private:
base::test::ScopedFeatureList features_;
};
TEST_F(AdAuctionServiceImplFacilitatedTestingTest,
RunAdAuctionServesDeprecationLabelsInKVRequest) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
return {'ad': 'example', 'bid': 1, 'render': 'https://example.com/render'};
}
)";
constexpr char kDecisionScript[] = R"(
function scoreAd(
adMetadata, bid, auctionConfig, trustedScoringSignals, browserSignals) {
return bid;
}
)";
bool bidding_kv_called = false;
bool scoring_kv_called = false;
network_responder_->RegisterScriptResponse(kBiddingUrlPath, kBiddingScript);
network_responder_->RegisterScriptResponse(kDecisionUrlPath, kDecisionScript);
network_responder_->RegisterSignalsResponse(
kTrustedBiddingSignalsUrlPath,
base::BindLambdaForTesting(
[&](URLLoaderInterceptor::RequestParams* params) {
std::string got_label;
EXPECT_TRUE(params->url_request.headers.GetHeader(
"Sec-Cookie-Deprecation", &got_label));
EXPECT_EQ("LabelForTesting", got_label);
bidding_kv_called = true;
URLLoaderInterceptor::WriteResponse(kFledgeSignalsHeaders, "{}",
params->client.get());
}));
network_responder_->RegisterSignalsResponse(
kTrustedScoringSignalsUrlPath,
base::BindLambdaForTesting(
[&](URLLoaderInterceptor::RequestParams* params) {
std::string got_label;
EXPECT_TRUE(params->url_request.headers.GetHeader(
"Sec-Cookie-Deprecation", &got_label));
EXPECT_EQ("LabelForTesting", got_label);
scoring_kv_called = true;
URLLoaderInterceptor::WriteResponse(kFledgeSignalsHeaders, "{}",
params->client.get());
}));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.bidding_url = kUrlA.Resolve(kBiddingUrlPath);
interest_group.trusted_bidding_signals_url = kTrustedBiddingSignalsUrlA;
interest_group.trusted_bidding_signals_keys = {"foo", "bar"};
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/std::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
blink::AuctionConfig auction_config;
auction_config.seller = kOriginA;
auction_config.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
auction_config.trusted_scoring_signals_url = kTrustedScoringSignalsUrlA;
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
std::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, std::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render"));
EXPECT_TRUE(bidding_kv_called);
EXPECT_TRUE(scoring_kv_called);
}
} // namespace content