blob: 44faf4d7514ffc4cfcbe6f1d773745f97b8bb598 [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 <string>
#include <vector>
#include "base/barrier_closure.h"
#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/lock.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/thread_annotations.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/fenced_frame/fenced_frame_url_mapping.h"
#include "content/browser/interest_group/auction_process_manager.h"
#include "content/browser/interest_group/interest_group_manager_impl.h"
#include "content/browser/interest_group/interest_group_storage.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/private_aggregation_features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.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 "mojo/public/cpp/bindings/remote.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/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 "url/gurl.h"
#include "url/origin.h"
namespace content {
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 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 kDailyUpdateUrlPath[] =
"/interest_group/daily_update_partial.json";
constexpr char kDailyUpdateUrlPath2[] =
"/interest_group/daily_update_partial_2.json";
constexpr char kDailyUpdateUrlPath3[] =
"/interest_group/daily_update_partial_3.json";
constexpr char kDailyUpdateUrlPath4[] =
"/interest_group/daily_update_partial_4.json";
constexpr char kDailyUpdateUrlPathB[] =
"/interest_group/daily_update_partial_b.json";
constexpr char kDailyUpdateUrlPathC[] =
"/interest_group/daily_update_partial_c.json";
// 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, 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() == "no.update.test" &&
api_origin.host() == "no.update.test");
}
};
constexpr char kFledgeUpdateHeaders[] =
"HTTP/1.1 200 OK\n"
"Content-type: Application/JSON\n"
"X-Allow-FLEDGE: true\n";
constexpr char kFledgeScriptHeaders[] =
"HTTP/1.1 200 OK\n"
"Content-type: Application/Javascript\n"
"X-Allow-FLEDGE: true\n";
constexpr char kFledgeReportHeaders[] =
"HTTP/1.1 200 OK\n"
"X-Allow-FLEDGE: 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:
// 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;
}
// Registers a URL to use a "deferred" update response. For a deferred
// response, the request handler returns true without a write, and writes are
// performed later in DoDeferredWrite() using a "stolen" Mojo pipe to the
// URLLoaderClient.
//
// It is valid to have a "deferred" response that never completes before the
// test exits.
void RegisterDeferredUpdateResponse(const std::string& url_path) {
base::AutoLock auto_lock(lock_);
CHECK(
deferred_update_responses_map_
.emplace(url_path, mojo::Remote<network::mojom::URLLoaderClient>())
.second);
}
// Perform the deferred response for `url_path` -- the test fails if the
// client isn't waiting on `url_path` registered with
// RegisterDeferredUpdateResponse().
void DoDeferredUpdateResponse(const std::string& url_path,
const std::string& response) {
base::AutoLock auto_lock(lock_);
auto it = deferred_update_responses_map_.find(url_path);
CHECK(it != deferred_update_responses_map_.end());
mojo::Remote<network::mojom::URLLoaderClient>& url_loader_client =
it->second;
CHECK(url_loader_client.is_bound());
URLLoaderInterceptor::WriteResponse(kFledgeUpdateHeaders, response,
url_loader_client.get());
deferred_update_responses_map_.erase(it);
}
// 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_);
// 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;
}
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.
update_count_++;
EXPECT_TRUE(params->url_request.trusted_params);
EXPECT_TRUE(params->url_request.trusted_params->isolation_info
.network_isolation_key()
.IsTransient());
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;
}
const auto deferred_update_it =
deferred_update_responses_map_.find(params->url_request.url.path());
if (deferred_update_it != deferred_update_responses_map_.end()) {
CHECK(!deferred_update_it->second);
deferred_update_it->second = std::move(params->client);
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();
}
// 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_);
// Only saves reporting requests that auctually received responses.
std::vector<std::string> sent_reports_ GUARDED_BY(lock_);
// Stores the set of URL paths that will receive deferred updates.
//
// First, a URL path is registered to receive an update, 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, mojo::Remote<network::mojom::URLLoaderClient>>
deferred_update_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},
/*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(),
&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();
RenderViewHostTestHarness::TearDown();
}
std::vector<StorageInterestGroup> GetInterestGroupsForOwner(
const url::Origin& owner) {
std::vector<StorageInterestGroup> interest_groups;
base::RunLoop run_loop;
manager_->GetInterestGroupsForOwner(
owner, base::BindLambdaForTesting(
[&run_loop, &interest_groups](
std::vector<StorageInterestGroup> groups) {
interest_groups = std::move(groups);
run_loop.Quit();
}));
run_loop.Run();
return interest_groups;
}
// Returns the specified interest group, if it exists.
absl::optional<StorageInterestGroup> GetInterestGroup(
const url::Origin& owner,
const std::string& name) {
for (auto& interest_group : GetInterestGroupsForOwner(owner)) {
if (interest_group.interest_group.name == name) {
return std::move(interest_group);
}
}
return absl::nullopt;
}
int GetJoinCount(const url::Origin& owner, const std::string& name) {
auto interest_group = GetInterestGroup(owner, name);
if (!interest_group)
return 0;
return interest_group->bidding_browser_signals->join_count;
}
double GetPriority(const url::Origin& owner, const std::string& name) {
auto interest_group = GetInterestGroup(owner, name);
if (!interest_group)
return 0;
return interest_group->interest_group.priority;
}
// Retrieves the FencedFrameProperties for the specified URN from the main
// frame. Returns nullopt if no such URN exists.
absl::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();
}
absl::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 absl::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
// JoinInterestGroupAndExpectPipeClosed().
EXPECT_TRUE(interest_service.is_bound());
EXPECT_TRUE(interest_service.is_connected());
}
// Attempt to join an interest group and expect the Mojo pipe to be closed.
// 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 JoinInterestGroupAndExpectPipeClosed(
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.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();
}
// 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
// LeaveInterestGroupAndExpectPipeClosed().
EXPECT_TRUE(interest_service.is_bound());
EXPECT_TRUE(interest_service.is_connected());
}
// Analogous to JoinInterestGroupAndExpectPipeClosed(), but leaves an interest
// group instead of joining one.
void LeaveInterestGroupAndExpectPipeClosed(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.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();
}
// 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 absl::nullopt if no ad won the auction.
absl::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;
absl::optional<blink::FencedFrame::RedactedFencedFrameConfig> maybe_config;
ad_auction_service_->RunAdAuction(
auction_config, mojo::NullReceiver(),
base::BindLambdaForTesting(
[&run_loop, &maybe_config](
bool manually_aborted,
const absl::optional<
blink::FencedFrame::RedactedFencedFrameConfig>& config) {
EXPECT_FALSE(manually_aborted);
maybe_config = config;
run_loop.Quit();
}));
ad_auction_service_.FlushForTesting();
run_loop.Run();
if (!maybe_config) {
return absl::nullopt;
}
CHECK(maybe_config->urn_uuid().has_value());
return maybe_config->urn_uuid();
}
// Like RunAdAuctionAndFlushForFrame(), but uses the RenderFrameHost of the
// main frame.
absl::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 absl::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 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 kUpdateUrlA = kUrlA.Resolve(kDailyUpdateUrlPath);
const GURL kUpdateUrlA2 = kUrlA.Resolve(kDailyUpdateUrlPath2);
const GURL kUpdateUrlA3 = kUrlA.Resolve(kDailyUpdateUrlPath3);
const GURL kUpdateUrlA4 = kUrlA.Resolve(kDailyUpdateUrlPath4);
const GURL kUpdateUrlB = kUrlB.Resolve(kDailyUpdateUrlPathB);
const GURL kUpdateUrlC = kUrlC.Resolve(kDailyUpdateUrlPathC);
const GURL kUpdateUrlNoUpdate = kUrlNoUpdate.Resolve(kDailyUpdateUrlPath);
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();
JoinInterestGroupAndExpectPipeClosed(interest_group);
EXPECT_EQ(0, GetJoinCount(interest_group.owner, kInterestGroupName));
// Try to join a same-origin HTTP interest group.
interest_group.owner = kHttpOriginA;
JoinInterestGroupAndExpectPipeClosed(interest_group);
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/"));
JoinInterestGroupAndExpectPipeClosed(interest_group);
EXPECT_EQ(0, GetJoinCount(interest_group.owner, kInterestGroupName));
// Secure, but not HTTPS.
interest_group.owner = url::Origin::Create(GURL("wss://a.test/"));
JoinInterestGroupAndExpectPipeClosed(interest_group);
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;
JoinInterestGroupAndExpectPipeClosed(interest_group);
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
// Test `daily_update_url`.
interest_group = CreateInterestGroup();
interest_group.daily_update_url = kBadUrl;
JoinInterestGroupAndExpectPipeClosed(interest_group);
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
// Test `trusted_bidding_signals_url`.
interest_group = CreateInterestGroup();
interest_group.trusted_bidding_signals_url = kBadUrl;
JoinInterestGroupAndExpectPipeClosed(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');
JoinInterestGroupAndExpectPipeClosed(interest_group);
EXPECT_EQ(0, GetJoinCount(kOriginA, kInterestGroupName));
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 0u);
}
// Non-HTTPS interest groups should be rejected, and result in the pipe being
// closed.
TEST_F(AdAuctionServiceImplTest, LeaveInterestGroupOriginNotHttps) {
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);
LeaveInterestGroupAndExpectPipeClosed(kOriginA, kInterestGroupName);
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);
auto storage_interest_group = GetInterestGroup(kOriginA, kInterestGroupName);
ASSERT_TRUE(storage_interest_group);
EXPECT_EQ(1, storage_interest_group->bidding_browser_signals->join_count);
EXPECT_EQ(interest_group.expiry,
storage_interest_group->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);
storage_interest_group = GetInterestGroup(kOriginA, kInterestGroupName);
ASSERT_TRUE(storage_interest_group);
EXPECT_EQ(2, storage_interest_group->bidding_browser_signals->join_count);
EXPECT_EQ(interest_group.expiry,
storage_interest_group->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);
storage_interest_group = GetInterestGroup(kOriginA, kInterestGroupName);
ASSERT_TRUE(storage_interest_group);
EXPECT_EQ(3, storage_interest_group->bidding_browser_signals->join_count);
base::Time actual_expiry = storage_interest_group->interest_group.expiry;
EXPECT_EQ(base::Time::Now() + kMaxExpiry, actual_expiry);
EXPECT_NE(interest_group.expiry, actual_expiry);
}
// These tests validate the `dailyUpdateUrl` and
// navigator.updateAdInterestGroups() functionality.
// The server JSON updates all fields that can be updated.
TEST_F(AdAuctionServiceImplTest, UpdateAllUpdatableFields) {
network_responder_->RegisterUpdateResponse(
kDailyUpdateUrlPath,
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": ["latencyStats"], "*": ["interestGroupCounts"]},
"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"],
"ads": [{"renderUrl": "%s/new_ad_render_url",
"metadata": {"new_a": "b"}
}],
"adComponents": [{"renderUrl": "https://example.com/component_url",
"metadata": {"new_c": "d"}
}]
})",
kOriginStringA, kOriginStringA, kOriginStringA,
kOriginStringA, kOriginStringA));
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::InterestGroup::SellerCapabilities::kInterestGroupCounts));
interest_group.all_sellers_capabilities =
blink::InterestGroup::SellerCapabilities::kLatencyStats;
interest_group.daily_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();
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[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::InterestGroup::SellerCapabilities::kInterestGroupCounts);
ASSERT_TRUE(group.seller_capabilities);
ASSERT_EQ(group.seller_capabilities->size(), 1u);
EXPECT_EQ(group.seller_capabilities->at(kOriginA),
blink::InterestGroup::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");
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
base::StringPrintf("%s/new_ad_render_url", kOriginStringA));
EXPECT_EQ(group.ads.value()[0].metadata, "{\"new_a\":\"b\"}");
ASSERT_TRUE(group.ad_components.has_value());
ASSERT_EQ(group.ad_components->size(), 1u);
EXPECT_EQ(group.ad_components.value()[0].render_url.spec(),
"https://example.com/component_url");
EXPECT_EQ(group.ad_components.value()[0].metadata, "{\"new_c\":\"d\"}");
}
// Only set the ads field -- the other fields shouldn't be changed.
TEST_F(AdAuctionServiceImplTest, UpdatePartialPerformsMerge) {
network_responder_->RegisterUpdateResponse(
kDailyUpdateUrlPath, 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.daily_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();
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[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.daily_update_url.has_value());
EXPECT_EQ(group.daily_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.spec(),
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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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.
const auto groups_before_update = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups_before_update.size(), 1u);
const base::Time kExpirationTime =
groups_before_update[0].interest_group.expiry;
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The expiration time shouldn't change.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[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.spec(),
"https://example.com/new_render");
}
// Only set the ads field -- the other fields shouldn't be changed.
TEST_F(AdAuctionServiceImplTest, UpdateSucceedsIfOptionalNameOwnerMatch) {
network_responder_->RegisterUpdateResponse(
kDailyUpdateUrlPath,
base::StringPrintf(R"({
"name": "%s",
"owner": "%s",
"ads": [{"renderUrl": "%s/new_ad_render_url"
}]
})",
kInterestGroupName, kOriginStringA, kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[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.daily_update_url.has_value());
EXPECT_EQ(group.daily_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.spec(),
base::StringPrintf("%s/new_ad_render_url", kOriginStringA));
}
// 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(kDailyUpdateUrlPath, R"({
"name": "boats",
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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(
kDailyUpdateUrlPath, base::StringPrintf(R"({
"owner": "%s",
"ads": [{"renderUrl": "%s/new_ad_render_url"
}]
})",
kOriginStringB, kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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.daily_update_url = kUpdateUrlA;
interest_group.expiry = base::Time::Now() + base::Days(30);
JoinInterestGroupAndFlush(interest_group);
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
EXPECT_EQ(groups[0].interest_group.priority_vector, absl::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(
kDailyUpdateUrlPath,
base::StringPrintf(R"({"priorityVector": %s})",
test_case.priority_vector_value));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
EXPECT_EQ(groups[0].interest_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.daily_update_url = kUpdateUrlA;
interest_group.expiry = base::Time::Now() + base::Days(30);
JoinInterestGroupAndFlush(interest_group);
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
EXPECT_EQ(groups[0].interest_group.priority_signals_overrides, absl::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(
kDailyUpdateUrlPath,
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[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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render1"}]
})");
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath2, R"({
"ads": [{"renderUrl": "https://example.com/new_render2"}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.name = kGroupName1;
interest_group.daily_update_url = kUrlA.Resolve(kDailyUpdateUrlPath);
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=*/absl::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.daily_update_url = kUrlA.Resolve(kDailyUpdateUrlPath2);
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=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 2u);
const auto& first_group = groups[0].interest_group.name == kGroupName1
? groups[0].interest_group
: groups[1].interest_group;
const auto& second_group = groups[0].interest_group.name == kGroupName2
? groups[0].interest_group
: groups[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.spec(),
"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.spec(),
"https://example.com/new_render2");
}
// 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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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.daily_update_url = kUrlB.Resolve(kDailyUpdateUrlPath);
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=*/absl::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...
std::vector<StorageInterestGroup> origin_b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(origin_b_groups.size(), 1u);
const auto& origin_b_group = origin_b_groups[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.spec(),
"https://example.com/new_render");
// ...but the `kOriginA` interest group shouldn't change.
std::vector<StorageInterestGroup> origin_a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(origin_a_groups.size(), 1u);
const auto& origin_a_group = origin_a_groups[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.spec(),
"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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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.daily_update_url = kUrlB.Resolve(kDailyUpdateUrlPath);
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=*/absl::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.daily_update_url = kUrlC.Resolve(kDailyUpdateUrlPath);
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=*/absl::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...
std::vector<StorageInterestGroup> origin_c_groups =
GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(origin_c_groups.size(), 1u);
const auto& origin_c_group = origin_c_groups[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.spec(),
"https://example.com/new_render");
// ...but the `kOriginA` interest group shouldn't change.
std::vector<StorageInterestGroup> origin_a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(origin_a_groups.size(), 1u);
const auto& origin_a_group = origin_a_groups[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.spec(),
"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.
std::vector<StorageInterestGroup> origin_b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(origin_b_groups.size(), 1u);
const auto& origin_b_group = origin_b_groups[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.spec(),
"https://example.com/render");
}
// The `ads` field is valid, but the ad `renderUrl` field is an invalid
// URL. The entire update should get cancelled, since updates are atomic.
TEST_F(AdAuctionServiceImplTest, UpdateInvalidFieldCancelsAllUpdates) {
network_responder_->RegisterUpdateResponse(
kDailyUpdateUrlPath, base::StringPrintf(R"({
"biddingLogicUrl": "%s/interest_group/new_bidding_logic.js",
"ads": [{"renderUrl": "https://invalid^&",
"metadata": {"new_a": "b"}
}]
})",
kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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 ads and bidding logic URL didn't change.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"https://example.com/render");
EXPECT_EQ(group.ads.value()[0].metadata,
"{\"ad\":\"metadata\",\"here\":[1,2,3]}");
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(
kDailyUpdateUrlPath, 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.daily_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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
EXPECT_EQ(group.priority, 2.0);
EXPECT_EQ(group.bidding_url, kBiddingLogicUrlA);
}
// The `sellerCapabilities` field has an invalid capability. The entire update
// should get cancelled, since updates are atomic.
TEST_F(AdAuctionServiceImplTest,
UpdateInvalidSellerCapabilitiesCancelsAllUpdates) {
network_responder_->RegisterUpdateResponse(
kDailyUpdateUrlPath, base::StringPrintf(R"({
"sellerCapabilities": {"%s": ["latencyStats"], "*": ["interestGroupCounts",
"invalidCapability"]},
"biddingLogicUrl": "%s/interest_group/new_bidding_logic.js"
})",
kOriginStringA, kOriginStringA));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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 and bidding logic URL didn't change.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
EXPECT_EQ(group.all_sellers_capabilities,
blink::InterestGroup::SellerCapabilitiesType());
EXPECT_FALSE(group.seller_capabilities);
EXPECT_EQ(group.bidding_url, kBiddingLogicUrlA);
}
// The server response can't be parsed as valid JSON. The update is cancelled.
TEST_F(AdAuctionServiceImplTest, UpdateInvalidJSONIgnored) {
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath,
"This isn't JSON.");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"https://example.com/render");
}
// UpdateJSONParserCrash fails on Android because Android doesn't use a separate
// process to parse JSON -- instead, it validates JSON in-process in Java, then,
// if validation succeeded, uses the C++ JSON parser, also in-proc. On other
// platforms, the C++ parser runs out-of-proc for safety.
#if !BUILDFLAG(IS_ANDROID)
// 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(kDailyUpdateUrlPath, 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.daily_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=*/absl::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.service().SimulateJsonParserCrashForTesting(
/*drop=*/true);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that the ads didn't change.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
auto group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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.service().SimulateJsonParserCrashForTesting(
/*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[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = kOriginNoUpdate;
interest_group.daily_update_url = kUpdateUrlNoUpdate;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginNoUpdate, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginNoUpdate);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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.daily_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=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"https://example.com/render");
}
// The network request for updating interest groups times out, so the update
// fails.
TEST_F(AdAuctionServiceImplTest, UpdateTimeout) {
network_responder_->RegisterDeferredUpdateResponse(kDailyUpdateUrlPath);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath);
// 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.daily_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=*/absl::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(kDailyUpdateUrlPath,
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(kDailyUpdateUrlPath,
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(kDailyUpdateUrlPath);
// 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.daily_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=*/absl::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(kDailyUpdateUrlPath,
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(kDailyUpdateUrlPath,
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(kDailyUpdateUrlPath);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.expiry = base::Time::Now() + base::Days(30);
interest_group.daily_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=*/absl::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.
std::vector<StorageInterestGroup> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups.size(), 1u);
auto a_group = a_groups[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.spec(),
"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(kDailyUpdateUrlPath, 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=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
blink::InterestGroupKey originA_group_key(kOriginA, kInterestGroupName);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_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=*/absl::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, "{}");
std::vector<StorageInterestGroup> prev_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(prev_groups.size(), 1u);
const auto& prev_signals = prev_groups[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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
const auto& signals = groups[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.spec(),
"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(kDailyUpdateUrlPath, 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"https://example.com/new_render");
// Change the update response and try updating again.
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
std::vector<StorageInterestGroup> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2.size(), 1u);
const auto& group2 = groups2[0].interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group2.ads.value()[0].render_url.spec(),
"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.
std::vector<StorageInterestGroup> groups3 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups3.size(), 1u);
const auto& group3 = groups3[0].interest_group;
ASSERT_TRUE(group3.ads.has_value());
ASSERT_EQ(group3.ads->size(), 1u);
EXPECT_EQ(group3.ads.value()[0].render_url.spec(),
"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.
std::vector<StorageInterestGroup> groups4 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups4.size(), 1u);
const auto& group4 = groups4[0].interest_group;
ASSERT_TRUE(group4.ads.has_value());
ASSERT_EQ(group4.ads->size(), 1u);
EXPECT_EQ(group4.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath,
"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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"https://example.com/render");
// Change the update response and try updating again.
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
std::vector<StorageInterestGroup> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2.size(), 1u);
const auto& group2 = groups2[0].interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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.
std::vector<StorageInterestGroup> groups3 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups3.size(), 1u);
const auto& group3 = groups3[0].interest_group;
ASSERT_TRUE(group3.ads.has_value());
ASSERT_EQ(group3.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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.
std::vector<StorageInterestGroup> groups4 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups4.size(), 1u);
const auto& group4 = groups4[0].interest_group;
ASSERT_TRUE(group4.ads.has_value());
ASSERT_EQ(group4.ads->size(), 1u);
EXPECT_EQ(group4.ads.value()[0].render_url.spec(),
"https://example.com/new_render");
}
// 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"https://example.com/render");
// Change the update response and try updating again.
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update does nothing due to rate limiting, nothing changes.
std::vector<StorageInterestGroup> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2.size(), 1u);
const auto& group2 = groups2[0].interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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.
std::vector<StorageInterestGroup> groups3 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups3.size(), 1u);
const auto& group3 = groups3[0].interest_group;
ASSERT_TRUE(group3.ads.has_value());
ASSERT_EQ(group3.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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.
std::vector<StorageInterestGroup> groups4 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups4.size(), 1u);
const auto& group4 = groups4[0].interest_group;
ASSERT_TRUE(group4.ads.has_value());
ASSERT_EQ(group4.ads->size(), 1u);
EXPECT_EQ(group4.ads.value()[0].render_url.spec(),
"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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"https://example.com/render");
// Change the update response and try updating again.
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// The update changes the database contents -- no rate limiting occurs.
std::vector<StorageInterestGroup> groups2 =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups2.size(), 1u);
const auto& group2 = groups2[0].interest_group;
ASSERT_TRUE(group2.ads.has_value());
ASSERT_EQ(group2.ads->size(), 1u);
EXPECT_EQ(group2.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath);
blink::InterestGroup interest_group_1 = CreateInterestGroup();
interest_group_1.expiry = base::Time::Now() + base::Days(30);
interest_group_1.daily_update_url = kUpdateUrlA;
interest_group_1.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::nullopt);
interest_group_1.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group_1);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
network_responder_->FailUpdateRequestWithError(
kDailyUpdateUrlPath2, net::ERR_INTERNET_DISCONNECTED);
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.daily_update_url = kUpdateUrlA2;
interest_group_2.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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(kDailyUpdateUrlPath,
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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 2u);
auto group_2 = groups[0].interest_group.name == kInterestGroupName2
? groups[0].interest_group
: groups[1].interest_group;
ASSERT_TRUE(group_2.ads.has_value());
ASSERT_EQ(group_2.ads->size(), 1u);
EXPECT_EQ(group_2.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath,
kServerResponse1);
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath2,
kServerResponse2);
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
// Check that both groups updated.
groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 2u);
auto group_1 = groups[0].interest_group.name == kInterestGroupName
? groups[0].interest_group
: groups[1].interest_group;
group_2 = groups[0].interest_group.name == kInterestGroupName2
? groups[0].interest_group
: groups[1].interest_group;
ASSERT_TRUE(group_1.ads.has_value());
ASSERT_EQ(group_1.ads->size(), 1u);
EXPECT_EQ(group_1.ads.value()[0].render_url.spec(),
"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.spec(),
"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(kDailyUpdateUrlPath, 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[0].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath);
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathB, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathC, 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlB;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlC;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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();
std::vector<StorageInterestGroup> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups.size(), 1u);
auto a_group = a_groups[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.spec(),
"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();
std::vector<StorageInterestGroup> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups.size(), 1u);
auto b_group = b_groups[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.spec(),
"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();
std::vector<StorageInterestGroup> c_groups =
GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(c_groups.size(), 1u);
auto c_group = c_groups[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.spec(),
"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(kDailyUpdateUrlPath,
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[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.spec(),
"https://example.com/new_render");
// kOriginB's groups have updated.
b_groups = GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups.size(), 1u);
b_group = b_groups[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.spec(),
"https://example.com/new_render");
// kOriginC's groups have updated.
b_groups = GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(b_groups.size(), 1u);
b_group = b_groups[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.spec(),
"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(kDailyUpdateUrlPath, 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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);
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 3u);
for (size_t i = 0; i < groups.size(); i++) {
SCOPED_TRACE(i);
const auto& group = groups[i].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
network_responder_->FailUpdateRequestWithError(kDailyUpdateUrlPath2,
net::ERR_CONNECTION_RESET);
// We never respond to this -- just let it timeout.
network_responder_->RegisterDeferredUpdateResponse(kDailyUpdateUrlPath3);
// 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlA2;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlA3;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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);
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 3u);
for (size_t i = 0; i < groups.size(); i++) {
SCOPED_TRACE(i);
const auto& group = groups[i].interest_group;
ASSERT_TRUE(group.ads.has_value());
ASSERT_EQ(group.ads->size(), 1u);
if (group.daily_update_url == kUpdateUrlA) {
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"https://example.com/new_render");
} else {
EXPECT_EQ(group.ads.value()[0].render_url.spec(),
"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(kDailyUpdateUrlPath);
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathB, 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlB;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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();
std::vector<StorageInterestGroup> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups.size(), 1u);
auto a_group = a_groups[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.spec(),
"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();
std::vector<StorageInterestGroup> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups.size(), 1u);
auto b_group = b_groups[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.spec(),
"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(kDailyUpdateUrlPath,
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[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.spec(),
"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[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.spec(),
"https://example.com/render");
// Now, try updating kOriginB. The update should complete successfully.
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathB, 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[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.spec(),
"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(kDailyUpdateUrlPath);
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathB, 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlB;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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();
std::vector<StorageInterestGroup> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups.size(), 1u);
auto a_group = a_groups[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.spec(),
"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();
std::vector<StorageInterestGroup> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups.size(), 1u);
auto b_group = b_groups[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.spec(),
"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(kDailyUpdateUrlPath,
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[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.spec(),
"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[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.spec(),
"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.daily_update_url = kUpdateUrlC;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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(kDailyUpdateUrlPathC, 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[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.spec(),
"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[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.spec(),
"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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathB, 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.daily_update_url = kUpdateUrlA;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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.daily_update_url = kUpdateUrlB;
interest_group.ads.emplace();
ad = blink::InterestGroup::Ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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();
std::vector<StorageInterestGroup> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups.size(), 1u);
auto a_group = a_groups[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.spec(),
"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();
std::vector<StorageInterestGroup> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups.size(), 1u);
auto b_group = b_groups[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.spec(),
"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(kDailyUpdateUrlPath,
kServerResponse);
network_responder_->FailUpdateRequestWithError(kDailyUpdateUrlPath2,
net::ERR_CONNECTION_RESET);
network_responder_->RegisterDeferredUpdateResponse(kDailyUpdateUrlPath3);
network_responder_->RegisterDeferredUpdateResponse(kDailyUpdateUrlPath4);
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathB,
kServerResponse);
// Create interest groups for kOriginA.
for (const GURL& daily_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 = daily_update_url.path();
interest_group.daily_update_url = daily_update_url;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::nullopt);
interest_group.ads->emplace_back(std::move(ad));
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, /*name=*/daily_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.daily_update_url = kUpdateUrlB;
interest_group.ads.emplace();
blink::InterestGroup::Ad ad(
/*render_url=*/GURL("https://example.com/render"),
/*metadata=*/absl::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();
std::vector<StorageInterestGroup> a_groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups.size(), 4u);
bool seen_succeeded = false, seen_failed = false;
for (const auto& a_group : a_groups) {
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.spec(),
"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.spec(),
"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();
std::vector<StorageInterestGroup> b_groups =
GetInterestGroupsForOwner(kOriginB);
ASSERT_EQ(b_groups.size(), 1u);
auto b_group = b_groups[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.spec(),
"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(kDailyUpdateUrlPath3,
kServerResponse);
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups.size(), 4u);
for (const auto& a_group : a_groups) {
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.spec(),
"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(kDailyUpdateUrlPath4,
kServerResponse);
task_environment()->RunUntilIdle();
a_groups = GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(a_groups.size(), 4u);
for (const auto& a_group : a_groups) {
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.spec(),
"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[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.spec(),
"https://example.com/render");
// Now, try updating kOriginB. The update should complete successfully.
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathB, 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[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.spec(),
"https://example.com/render3");
}
// 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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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();
auto storage_interest_group =
GetInterestGroup(interest_group.owner, interest_group.name);
ASSERT_TRUE(storage_interest_group);
EXPECT_EQ(0, storage_interest_group->bidding_browser_signals->bid_count);
EXPECT_EQ(0u,
storage_interest_group->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);
storage_interest_group =
GetInterestGroup(interest_group.owner, interest_group.name);
ASSERT_TRUE(storage_interest_group);
EXPECT_EQ(1, storage_interest_group->bidding_browser_signals->bid_count);
ASSERT_EQ(1u,
storage_interest_group->bidding_browser_signals->prev_wins.size());
ASSERT_EQ(
R"({"render_url":"https://example.com/render"})",
storage_interest_group->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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, absl::nullopt);
// The bid count should be updated immediately, since theere's no URN to wait
// to be loaded in a frame.
auto storage_interest_group =
GetInterestGroup(interest_group.owner, interest_group.name);
ASSERT_TRUE(storage_interest_group);
EXPECT_EQ(1, storage_interest_group->bidding_browser_signals->bid_count);
EXPECT_EQ(0u,
storage_interest_group->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=*/absl::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);
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
// Auction failed because the number of urn mappings has reached limit.
ASSERT_EQ(auction_result, absl::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;
}
)";
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath, 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.daily_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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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[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.spec(),
"https://example.com/new_render");
}
// 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(kDailyUpdateUrlPath, 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.daily_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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, absl::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[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.spec(),
"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(kDailyUpdateUrlPath, 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.daily_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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, absl::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[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.spec(),
"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(kDailyUpdateUrlPath, 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.daily_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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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[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.spec(),
"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 on the top-level auction, and C is a buyer in the
// component auction. Force the inner auction to win by making it bid higher.
//
// Both interest groups should be updated after the auction completes.
TEST_F(AdAuctionServiceImplTest,
UpdatesInterestGroupsAfterComponentAuctionInnerWins) {
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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathC, 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.daily_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=*/absl::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.daily_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=*/absl::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);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
blink::AuctionConfig component_auction;
component_auction.seller = kOriginA;
component_auction.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction.non_shared_params.interest_group_buyers = {kOriginC};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction));
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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[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.spec(),
"https://example.com/new_render");
auto c_groups = GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(c_groups.size(), 1u);
auto c_group = c_groups[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.spec(),
"https://example.com/new_render");
}
// Like UpdatesInterestGroupsAfterComponentAuctionInnerWins, but the outer
// auction wins.
TEST_F(AdAuctionServiceImplTest,
UpdatesInterestGroupsAfterComponentAuctionOuterWins) {
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: bid, allowComponentAuction: true};
}
)";
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathC, 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.daily_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=*/absl::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.daily_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=*/absl::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);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
blink::AuctionConfig component_auction;
component_auction.seller = kOriginA;
component_auction.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction.non_shared_params.interest_group_buyers = {kOriginC};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction));
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::nullopt);
EXPECT_EQ(ConvertFencedFrameURNToURL(*auction_result),
GURL("https://example.com/render1"));
// 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[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.spec(),
"https://example.com/new_render");
auto c_groups = GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(c_groups.size(), 1u);
auto c_group = c_groups[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.spec(),
"https://example.com/new_render");
}
// Like UpdatesInterestGroupsAfterComponentAuctionInnerWins, but there's no
// winner, since the decision script scores every bid as 0.
//
// All participating interest groups should still update.
TEST_F(AdAuctionServiceImplTest,
UpdatesInterestGroupsAfterComponentAuctionNoWinner) {
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(kDailyUpdateUrlPath, R"({
"ads": [{"renderUrl": "https://example.com/new_render"
}]
})");
network_responder_->RegisterUpdateResponse(kDailyUpdateUrlPathC, 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.daily_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=*/absl::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.daily_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=*/absl::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);
auction_config.non_shared_params.interest_group_buyers = {kOriginA};
blink::AuctionConfig component_auction;
component_auction.seller = kOriginA;
component_auction.decision_logic_url = kUrlA.Resolve(kDecisionUrlPath);
component_auction.non_shared_params.interest_group_buyers = {kOriginC};
auction_config.non_shared_params.component_auctions.emplace_back(
std::move(component_auction));
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_EQ(auction_result, absl::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[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.spec(),
"https://example.com/new_render");
auto c_groups = GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(c_groups.size(), 1u);
auto c_group = c_groups[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.spec(),
"https://example.com/new_render");
}
// 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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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, absl::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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, absl::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, absl::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);
}
// Check that reports are sent (And there's no UAF when invoking the URN
// callback) if the AdAuctionService is deleted before a URN is navigated to.
//
// This isn't a common case, but can happen if an auction runs in an iframe,
// which is then closed, and then the URN is loaded in a fenced frame.
//
// It can also potentially happen when navigating away from a page that run an
// auction is racing against loading a URN in a frame within the page.
//
// It can also happen if a compromised renderer closes the Mojo pipe to its
// AdAuctionService.
TEST_F(AdAuctionServiceImplTest, ReportsSentAfterServiceDestruction) {
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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::nullopt);
// Destroy the auction service Mojo pipe, and wait for the underlying service
// to be destroyed.
DestroyAdAuctionService();
base::RunLoop().RunUntilIdle();
EXPECT_EQ(network_responder_->ReportCount(), 0u);
// Invoking the callback should not crash and should result in two reports
// being sent.
InvokeCallbackForURN(*auction_result);
network_responder_->WaitForNumReports(2);
}
// 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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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);
manager_->set_max_report_queue_length_for_testing(1);
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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::nullopt);
InvokeCallbackForURN(*auction_result);
}
// 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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::nullopt);
InvokeCallbackForURN(*auction_result);
// 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, absl::nullopt);
InvokeCallbackForURN(*auction_result);
// 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, absl::nullopt);
InvokeCallbackForURN(*auction_result);
task_environment()->FastForwardBy(base::Seconds(20));
// Two more reports from the third auction are sent.
EXPECT_EQ(network_responder_->ReportCount(), 3u);
}
// 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=*/absl::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), absl::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), absl::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), absl::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), absl::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), absl::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), absl::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), absl::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=*/absl::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), absl::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), absl::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=*/absl::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), absl::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 absl::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 absl::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 absl::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 absl::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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::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=*/absl::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), absl::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), absl::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), absl::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=*/absl::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 manually_aborted,
const absl::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);
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(
kDailyUpdateUrlPath,
base::StringPrintf(R"({"biddingLogicUrl": "%s%s"})", kOriginStringA,
kNewBiddingUrlPath));
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.daily_update_url = kUpdateUrlA;
interest_group.bidding_url = kBiddingLogicUrlA;
JoinInterestGroupAndFlush(interest_group);
EXPECT_EQ(1, GetJoinCount(kOriginA, kInterestGroupName));
UpdateInterestGroupNoFlush();
task_environment()->RunUntilIdle();
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[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(
kDailyUpdateUrlPath,
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.daily_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();
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginA);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[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.
// TODO(crbug.com/1404806): Flaky on all platforms
TEST_F(AdAuctionServiceImplRestrictedPermissionsPolicyTest,
DISABLED_APICallsFromCrossSiteIFrame) {
network_responder_->RegisterUpdateResponse(
kDailyUpdateUrlPath,
base::StringPrintf(R"({"biddingLogicUrl": "%s%s"})", kOriginStringC,
kNewBiddingUrlPath));
NavigateAndCommit(kUrlC);
blink::InterestGroup interest_group = CreateInterestGroup();
interest_group.owner = kOriginC;
interest_group.bidding_url = kUrlC.Resolve(kBiddingUrlPath);
JoinInterestGroupAndFlush(interest_group);
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;
JoinInterestGroupAndExpectPipeClosed(std::move(interest_group_2), subframe);
EXPECT_EQ(0, GetJoinCount(kOriginC, kInterestGroupName2));
UpdateInterestGroupNoFlushForFrame(subframe);
task_environment()->RunUntilIdle();
// `bidding_url` should not change.
std::vector<StorageInterestGroup> groups =
GetInterestGroupsForOwner(kOriginC);
ASSERT_EQ(groups.size(), 1u);
const auto& group = groups[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));
LeaveInterestGroupAndExpectPipeClosed(kOriginC, kInterestGroupName, 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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::nullopt);
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 AdAuctionServiceImplPrivateAggregationEnabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplPrivateAggregationEnabledTest() {
feature_list_.InitAndEnableFeature(content::kPrivateAggregationApi);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationReportForwarded) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.sendHistogramReport({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;
}
)";
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(AggregatableReportRequest,
PrivateAggregationBudgetKey)>
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=*/absl::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(mock_callback, Run)
.WillRepeatedly(
testing::Invoke([this](AggregatableReportRequest request,
PrivateAggregationBudgetKey budget_key) {
ASSERT_EQ(request.payload_contents().contributions.size(), 1u);
EXPECT_EQ(request.payload_contents().contributions[0].bucket, 1);
EXPECT_EQ(request.payload_contents().contributions[0].value, 2);
EXPECT_EQ(request.shared_info().reporting_origin, kOriginA);
EXPECT_EQ(budget_key.api(),
PrivateAggregationBudgetKey::Api::kFledge);
EXPECT_EQ(budget_key.origin(), kOriginA);
}));
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
EXPECT_NE(auction_result, absl::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,
PrivateAggregationUseCounterLogged) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.sendHistogramReport({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=*/absl::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));
EXPECT_CALL(
browser_client,
LogWebFeatureForCurrentPage(
testing::_, blink::mojom::WebFeature::kPrivateAggregationApiFledge));
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
ASSERT_NE(auction_result, absl::nullopt);
InvokeCallbackForURN(*auction_result);
}
// TODO(crbug.com/1356654): Update when use counter coverage is improved.
TEST_F(AdAuctionServiceImplPrivateAggregationEnabledTest,
PrivateAggregationUseCounterNotLoggedOnFailedInvocation) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.sendHistogramReport({});
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=*/absl::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);
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
// There should've been a sendHistogramReport() error.
EXPECT_EQ(auction_result, absl::nullopt);
}
class AdAuctionServiceImplPrivateAggregationDisabledTest
: public AdAuctionServiceImplTest {
public:
AdAuctionServiceImplPrivateAggregationDisabledTest() {
feature_list_.InitAndDisableFeature(content::kPrivateAggregationApi);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(AdAuctionServiceImplPrivateAggregationDisabledTest,
PrivateAggregationNotExposed) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.sendHistogramReport({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=*/absl::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};
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
// privateAggregation should cause a ReferenceError.
EXPECT_EQ(auction_result, absl::nullopt);
}
TEST_F(AdAuctionServiceImplPrivateAggregationDisabledTest,
PrivateAggregationUseCounterNotLogged) {
constexpr char kBiddingScript[] = R"(
function generateBid(
interestGroup, auctionSignals, perBuyerSignals, trustedBiddingSignals,
browserSignals) {
privateAggregation.sendHistogramReport({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=*/absl::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);
absl::optional<GURL> auction_result = RunAdAuctionAndFlush(auction_config);
// privateAggregation should cause a ReferenceError.
EXPECT_EQ(auction_result, absl::nullopt);
}
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=*/absl::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};
absl::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, absl::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();
auto storage_interest_group =
GetInterestGroup(interest_group.owner, interest_group.name);
ASSERT_TRUE(storage_interest_group);
EXPECT_EQ(0, storage_interest_group->bidding_browser_signals->bid_count);
EXPECT_EQ(
0u,
storage_interest_group->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);
storage_interest_group =
GetInterestGroup(interest_group.owner, interest_group.name);
ASSERT_TRUE(storage_interest_group);
EXPECT_EQ(1, storage_interest_group->bidding_browser_signals->bid_count);
ASSERT_EQ(
1u,
storage_interest_group->bidding_browser_signals->prev_wins.size());
ASSERT_EQ(R"({"render_url":"https://example.com/render"})",
storage_interest_group->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));
} // namespace content