blob: c330cc8b6f63f0c1f4ad9e80bc32f9f949097f35 [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/services/auction_worklet/bidder_worklet.h"
#include <stdint.h>
#include <string>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/memory/raw_ptr.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "content/services/auction_worklet/auction_v8_helper.h"
#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom-forward.h"
#include "content/services/auction_worklet/public/mojom/private_aggregation_request.mojom.h"
#include "content/services/auction_worklet/worklet_devtools_debug_test_util.h"
#include "content/services/auction_worklet/worklet_test_util.h"
#include "content/services/auction_worklet/worklet_v8_debug_test_util.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_associated_receiver.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "mojo/public/cpp/bindings/unique_receiver_set.h"
#include "net/http/http_status_code.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/numeric/int128.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/interest_group/ad_auction_constants.h"
#include "third_party/blink/public/common/interest_group/ad_auction_currencies.h"
#include "third_party/blink/public/common/interest_group/interest_group.h"
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
using testing::HasSubstr;
using testing::StartsWith;
namespace auction_worklet {
namespace {
using base::test::TaskEnvironment;
using PrivateAggregationRequests = BidderWorklet::PrivateAggregationRequests;
// This was produced by running wat2wasm on this:
// (module
// (global (export "test_const") i32 (i32.const 123))
// )
const uint8_t kToyWasm[] = {
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x06, 0x07, 0x01,
0x7f, 0x00, 0x41, 0xfb, 0x00, 0x0b, 0x07, 0x0e, 0x01, 0x0a, 0x74,
0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x03, 0x00};
const char kWasmUrl[] = "https://foo.test/helper.wasm";
// Packs kToyWasm into a std::string.
std::string ToyWasm() {
return std::string(reinterpret_cast<const char*>(kToyWasm),
std::size(kToyWasm));
}
// Creates generateBid() scripts with the specified result value, in raw
// Javascript. Allows returning generateBid() arguments, arbitrary values,
// incorrect types, etc.
std::string CreateGenerateBidScript(const std::string& raw_return_value,
const std::string& extra_code = "") {
constexpr char kGenerateBidScript[] = R"(
function generateBid(interestGroup, auctionSignals, perBuyerSignals,
trustedBiddingSignals, browserSignals,
directFromSellerSignals) {
%s;
return %s;
}
)";
return base::StringPrintf(kGenerateBidScript, extra_code.c_str(),
raw_return_value.c_str());
}
// Returns a working script, primarily for testing failure cases where it
// should not be run.
static std::string CreateBasicGenerateBidScript() {
return CreateGenerateBidScript(
R"({ad: ["ad"], bid:1, render:"https://response.test/"})");
}
// Returns a working script which calls forDebuggingOnly.reportAdAuctionLoss()
// and forDebuggingOnly.reportAdAuctionWin() if corresponding url is provided.
static std::string CreateBasicGenerateBidScriptWithDebuggingReport(
const std::string& extra_code) {
return CreateGenerateBidScript(
R"({ad: ["ad"], bid:1, render:"https://response.test/"})", extra_code);
}
// Creates bidder worklet script with a reportWin() function with the specified
// body, and the default generateBid() function.
std::string CreateReportWinScript(const std::string& function_body) {
constexpr char kReportWinScript[] = R"(
function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
browserSignals, directFromSellerSignals) {
%s;
}
)";
return CreateBasicGenerateBidScript() +
base::StringPrintf(kReportWinScript, function_body.c_str());
}
// A GenerateBidClient that takes a callback to call in OnGenerateBid(), and
// optionally one for OnBiddingSignalsReceived(). If no callback for
// OnBiddingSignalsReceived() is provided, just invokes the
// OnBiddingSignalsReceivedCallback immediately.
class GenerateBidClientWithCallbacks : public mojom::GenerateBidClient {
public:
using OnBiddingSignalsReceivedCallback = base::OnceCallback<void(
const base::flat_map<std::string, double>& priority_vector,
base::TimeDelta trusted_signals_fetch_latency,
base::OnceClosure callback)>;
using GenerateBidCallback = base::OnceCallback<void(
mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version,
bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority,
bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors)>;
explicit GenerateBidClientWithCallbacks(
GenerateBidCallback generate_bid_callback,
OnBiddingSignalsReceivedCallback on_bidding_signals_received_callback =
OnBiddingSignalsReceivedCallback())
: on_bidding_signals_received_callback_(
std::move(on_bidding_signals_received_callback)),
generate_bid_callback_(std::move(generate_bid_callback)) {
DCHECK(generate_bid_callback_);
}
~GenerateBidClientWithCallbacks() override = default;
// Helper that creates a GenerateBidClientWithCallbacks() using a
// SelfOwnedReceived.
static mojo::PendingAssociatedRemote<mojom::GenerateBidClient> Create(
GenerateBidCallback callback,
OnBiddingSignalsReceivedCallback on_bidding_signals_received_callback =
OnBiddingSignalsReceivedCallback()) {
mojo::PendingAssociatedRemote<mojom::GenerateBidClient> client_remote;
mojo::MakeSelfOwnedAssociatedReceiver(
std::make_unique<GenerateBidClientWithCallbacks>(
std::move(callback),
std::move(on_bidding_signals_received_callback)),
client_remote.InitWithNewEndpointAndPassReceiver());
return client_remote;
}
// Creates a GenerateBidClient() that expects OnGenerateBidComplete() never to
// be invoked. Allows OnBiddingSignalsReceived() to be invoked.
static mojo::PendingAssociatedRemote<mojom::GenerateBidClient>
CreateNeverCompletes() {
return Create(GenerateBidNeverInvokedCallback());
}
static GenerateBidCallback GenerateBidNeverInvokedCallback() {
return base::BindOnce(
[](mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version, bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority, bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) {
ADD_FAILURE() << "OnGenerateBidComplete should not be invoked.";
});
}
// mojom::GenerateBidClient implementation:
void OnBiddingSignalsReceived(
const base::flat_map<std::string, double>& priority_vector,
base::TimeDelta trusted_signals_fetch_latency,
base::OnceClosure callback) override {
// May only be called once.
EXPECT_FALSE(on_bidding_signals_received_invoked_);
on_bidding_signals_received_invoked_ = true;
if (on_bidding_signals_received_callback_) {
std::move(on_bidding_signals_received_callback_)
.Run(priority_vector, trusted_signals_fetch_latency,
std::move(callback));
return;
}
std::move(callback).Run();
}
void OnGenerateBidComplete(
mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version,
bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority,
bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) override {
// OnBiddingSignalsReceived() must be called first.
EXPECT_TRUE(on_bidding_signals_received_invoked_);
std::move(generate_bid_callback_)
.Run(std::move(bid), std::move(kanon_bid), data_version,
has_data_version, debug_loss_report_url, debug_win_report_url,
set_priority, has_set_priority,
std::move(update_priority_signals_overrides),
std::move(pa_requests), std::move(non_kanon_pa_requests),
bidding_latency, std::move(generate_bid_dependency_latencies),
reject_reason, errors);
}
private:
bool on_bidding_signals_received_invoked_ = false;
OnBiddingSignalsReceivedCallback on_bidding_signals_received_callback_;
GenerateBidCallback generate_bid_callback_;
};
class BidderWorkletTest : public testing::Test {
public:
BidderWorkletTest(TaskEnvironment::TimeSource time_source =
TaskEnvironment::TimeSource::SYSTEM_TIME)
: task_environment_(time_source) {
SetDefaultParameters();
}
~BidderWorkletTest() override = default;
void SetUp() override {
// v8_helper_ needs to be created here instead of the constructor, because
// this test fixture has a subclass that initializes a ScopedFeatureList in
// their constructor, which needs to be done BEFORE other threads are
// started in multithreaded test environments so that no other threads use
// it when it's being initiated.
// https://source.chromium.org/chromium/chromium/src/+/main:base/test/scoped_feature_list.h;drc=60124005e97ae2716b0fb34187d82da6019b571f;l=37
v8_helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
}
void TearDown() override {
// Release the V8 helper and process all pending tasks. This is to make sure
// there aren't any pending tasks between the V8 thread and the main thread
// that will result in UAFs. These lines are not necessary for any test to
// pass. This needs to be done before a subclass resets ScopedFeatureList,
// so no thread queries it while it's being modified.
v8_helper_.reset();
task_environment_.RunUntilIdle();
}
// Default values. No test actually depends on these being anything but valid,
// but test that set these can use this to reset values to default after each
// test.
void SetDefaultParameters() {
interest_group_name_ = "Fred";
reporting_id_field_ = mojom::ReportingIdField::kInterestGroupName;
reporting_id_ = interest_group_name_;
interest_group_enable_bidding_signals_prioritization_ = false;
interest_group_priority_vector_.reset();
interest_group_user_bidding_signals_ = absl::nullopt;
join_origin_ = url::Origin::Create(GURL("https://url.test/"));
execution_mode_ =
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode;
interest_group_ads_.clear();
interest_group_ads_.emplace_back(GURL("https://response.test/"),
/*metadata=*/absl::nullopt);
interest_group_ad_components_.reset();
interest_group_ad_components_.emplace();
interest_group_ad_components_->emplace_back(
GURL("https://ad_component.test/"), /*metadata=*/absl::nullopt);
kanon_keys_.clear();
kanon_mode_ = auction_worklet::mojom::KAnonymityBidMode::kNone;
provide_direct_from_seller_signals_late_ = false;
update_url_.reset();
interest_group_trusted_bidding_signals_url_.reset();
interest_group_trusted_bidding_signals_keys_.reset();
browser_signal_join_count_ = 2;
browser_signal_bid_count_ = 3;
browser_signal_prev_wins_.clear();
auction_signals_ = "[\"auction_signals\"]";
per_buyer_signals_ = "[\"per_buyer_signals\"]";
per_buyer_timeout_ = absl::nullopt;
per_buyer_currency_ = absl::nullopt;
top_window_origin_ = url::Origin::Create(GURL("https://top.window.test/"));
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/true,
/*shared_storage_allowed=*/true);
experiment_group_id_ = absl::nullopt;
browser_signal_seller_origin_ =
url::Origin::Create(GURL("https://browser.signal.seller.test/"));
browser_signal_top_level_seller_origin_.reset();
seller_signals_ = "[\"seller_signals\"]";
browser_signal_render_url_ = GURL("https://render_url.test/");
browser_signal_bid_ = 1;
browser_signal_bid_currency_ = absl::nullopt;
browser_signal_highest_scoring_other_bid_ = 0.5;
browser_signal_highest_scoring_other_bid_currency_ = absl::nullopt;
browser_signal_made_highest_scoring_other_bid_ = false;
browser_signal_ad_cost_.reset();
browser_signal_modeling_signals_.reset();
browser_signal_join_count_ = 2;
browser_signal_recency_ = 5;
data_version_.reset();
}
// Helper that creates and runs a script to validate that `expression`
// evaluates to true when evaluated in a generateBid() script. Does this by
// evaluating the expression in the content of generateBid() and throwing if
// it's not true. Otherwise, a bid is generated.
void RunGenerateBidExpectingExpressionIsTrue(const std::string& expression) {
std::string script = CreateGenerateBidScript(
/*raw_return_value=*/R"({bid: 1, render:"https://response.test/"})",
/*extra_code=*/base::StringPrintf(R"(let val = %s;
if (val !== true)
throw JSON.stringify(val);)",
expression.c_str()));
RunGenerateBidWithJavascriptExpectingResult(
script, mojom::BidderWorkletBid::New(
/*ad=*/"null",
/*bid=*/1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
// Configures `url_loader_factory_` to return a generateBid() script with the
// specified return line. Then runs the script, expecting the provided result.
void RunGenerateBidWithReturnValueExpectingResult(
const std::string& raw_return_value,
mojom::BidderWorkletBidPtr expected_bid,
const absl::optional<uint32_t>& expected_data_version = absl::nullopt,
std::vector<std::string> expected_errors = std::vector<std::string>(),
const absl::optional<GURL>& expected_debug_loss_report_url =
absl::nullopt,
const absl::optional<GURL>& expected_debug_win_report_url = absl::nullopt,
const absl::optional<double> expected_set_priority = absl::nullopt,
const base::flat_map<std::string, absl::optional<double>>&
expected_update_priority_signals_overrides =
base::flat_map<std::string, absl::optional<double>>(),
PrivateAggregationRequests expected_pa_requests = {},
PrivateAggregationRequests expected_non_kanon_pa_requests = {}) {
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(raw_return_value), std::move(expected_bid),
expected_data_version, expected_errors, expected_debug_loss_report_url,
expected_debug_win_report_url, expected_set_priority,
expected_update_priority_signals_overrides,
std::move(expected_pa_requests),
std::move(expected_non_kanon_pa_requests));
}
// Configures `url_loader_factory_` to return a script with the specified
// Javascript. Then runs the script, expecting the provided result.
void RunGenerateBidWithJavascriptExpectingResult(
const std::string& javascript,
mojom::BidderWorkletBidPtr expected_bid,
const absl::optional<uint32_t>& expected_data_version = absl::nullopt,
std::vector<std::string> expected_errors = std::vector<std::string>(),
const absl::optional<GURL>& expected_debug_loss_report_url =
absl::nullopt,
const absl::optional<GURL>& expected_debug_win_report_url = absl::nullopt,
const absl::optional<double> expected_set_priority = absl::nullopt,
const base::flat_map<std::string, absl::optional<double>>&
expected_update_priority_signals_overrides =
base::flat_map<std::string, absl::optional<double>>(),
PrivateAggregationRequests expected_pa_requests = {},
PrivateAggregationRequests expected_non_kanon_pa_requests = {}) {
SCOPED_TRACE(javascript);
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
javascript);
RunGenerateBidExpectingResult(
std::move(expected_bid), expected_data_version, expected_errors,
expected_debug_loss_report_url, expected_debug_win_report_url,
expected_set_priority, expected_update_priority_signals_overrides,
std::move(expected_pa_requests),
std::move(expected_non_kanon_pa_requests));
}
// Loads and runs a generateBid() script, expecting the provided result.
//
// `bid_duration` of `expected_bid` is ignored unless it's non-zero, in which
// case the duration is expected to be at least `bid_duration` - useful for
// testing that `bid_duration` at least seems to reflect timeouts.
void RunGenerateBidExpectingResult(
mojom::BidderWorkletBidPtr expected_bid,
const absl::optional<uint32_t>& expected_data_version = absl::nullopt,
std::vector<std::string> expected_errors = std::vector<std::string>(),
const absl::optional<GURL>& expected_debug_loss_report_url =
absl::nullopt,
const absl::optional<GURL>& expected_debug_win_report_url = absl::nullopt,
const absl::optional<double> expected_set_priority = absl::nullopt,
const base::flat_map<std::string, absl::optional<double>>&
expected_update_priority_signals_overrides =
base::flat_map<std::string, absl::optional<double>>(),
PrivateAggregationRequests expected_pa_requests = {},
PrivateAggregationRequests expected_non_kanon_pa_requests = {}) {
auto bidder_worklet = CreateWorkletAndGenerateBid();
EXPECT_EQ(expected_bid.is_null(), bid_.is_null());
if (expected_bid && bid_) {
EXPECT_EQ(expected_bid->ad, bid_->ad);
EXPECT_EQ(expected_bid->bid, bid_->bid);
EXPECT_EQ(blink::PrintableAdCurrency(expected_bid->bid_currency),
blink::PrintableAdCurrency(bid_->bid_currency));
EXPECT_EQ(expected_bid->ad_descriptor.url, bid_->ad_descriptor.url);
if (!expected_bid->ad_component_descriptors) {
EXPECT_FALSE(bid_->ad_component_descriptors);
} else {
EXPECT_THAT(*bid_->ad_component_descriptors,
::testing::ElementsAreArray(
*expected_bid->ad_component_descriptors));
}
if (!expected_bid->bid_duration.is_zero()) {
EXPECT_GE(bid_->bid_duration, expected_bid->bid_duration);
}
}
EXPECT_EQ(expected_data_version, data_version_);
EXPECT_EQ(expected_debug_loss_report_url, bid_debug_loss_report_url_);
EXPECT_EQ(expected_debug_win_report_url, bid_debug_win_report_url_);
EXPECT_EQ(expected_pa_requests, pa_requests_);
EXPECT_EQ(expected_non_kanon_pa_requests, non_kanon_pa_requests_);
EXPECT_EQ(expected_errors, bid_errors_);
EXPECT_EQ(expected_set_priority, set_priority_);
EXPECT_EQ(expected_update_priority_signals_overrides,
update_priority_signals_overrides_);
}
// Configures `url_loader_factory_` to return a reportWin() script with the
// specified body. Then runs the script, expecting the provided result.
void RunReportWinWithFunctionBodyExpectingResult(
const std::string& function_body,
const absl::optional<GURL>& expected_report_url,
const base::flat_map<std::string, GURL>& expected_ad_beacon_map =
base::flat_map<std::string, GURL>(),
PrivateAggregationRequests expected_pa_requests = {},
const std::vector<std::string>& expected_errors =
std::vector<std::string>()) {
RunReportWinWithJavascriptExpectingResult(
CreateReportWinScript(function_body), expected_report_url,
expected_ad_beacon_map, std::move(expected_pa_requests),
expected_errors);
}
// Configures `url_loader_factory_` to return a reportWin() script with the
// specified Javascript. Then runs the script, expecting the provided result.
void RunReportWinWithJavascriptExpectingResult(
const std::string& javascript,
const absl::optional<GURL>& expected_report_url,
const base::flat_map<std::string, GURL>& expected_ad_beacon_map =
base::flat_map<std::string, GURL>(),
PrivateAggregationRequests expected_pa_requests = {},
const std::vector<std::string>& expected_errors =
std::vector<std::string>()) {
SCOPED_TRACE(javascript);
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
javascript);
RunReportWinExpectingResult(expected_report_url, expected_ad_beacon_map,
std::move(expected_pa_requests),
expected_errors);
}
// Runs reportWin() on an already loaded worklet, verifies the return
// value and invokes `done_closure` when done. Expects something else to
// spin the event loop.
void RunReportWinExpectingResultAsync(
mojom::BidderWorklet* bidder_worklet,
const absl::optional<GURL>& expected_report_url,
const base::flat_map<std::string, GURL>& expected_ad_beacon_map,
PrivateAggregationRequests expected_pa_requests,
const std::vector<std::string>& expected_errors,
base::OnceClosure done_closure) {
bidder_worklet->ReportWin(
reporting_id_field_, reporting_id_, auction_signals_,
per_buyer_signals_, direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, seller_signals_,
browser_signal_render_url_, browser_signal_bid_,
browser_signal_bid_currency_, browser_signal_highest_scoring_other_bid_,
browser_signal_highest_scoring_other_bid_currency_,
browser_signal_made_highest_scoring_other_bid_, browser_signal_ad_cost_,
browser_signal_modeling_signals_, browser_signal_join_count_,
browser_signal_recency_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, data_version_.value_or(0),
data_version_.has_value(),
/*trace_id=*/1,
base::BindOnce(
[](const absl::optional<GURL>& expected_report_url,
const base::flat_map<std::string, GURL>& expected_ad_beacon_map,
PrivateAggregationRequests expected_pa_requests,
const std::vector<std::string>& expected_errors,
base::OnceClosure done_closure,
const absl::optional<GURL>& report_url,
const base::flat_map<std::string, GURL>& ad_beacon_map,
PrivateAggregationRequests pa_requests,
const std::vector<std::string>& errors) {
EXPECT_EQ(expected_report_url, report_url);
EXPECT_EQ(expected_errors, errors);
EXPECT_EQ(expected_ad_beacon_map, ad_beacon_map);
EXPECT_EQ(expected_pa_requests, pa_requests);
std::move(done_closure).Run();
},
expected_report_url, expected_ad_beacon_map,
std::move(expected_pa_requests), expected_errors,
std::move(done_closure)));
}
// Loads and runs a reportWin() with the provided return line, expecting the
// supplied result.
void RunReportWinExpectingResult(
const absl::optional<GURL>& expected_report_url,
const base::flat_map<std::string, GURL>& expected_ad_beacon_map =
base::flat_map<std::string, GURL>(),
PrivateAggregationRequests expected_pa_requests = {},
const std::vector<std::string>& expected_errors =
std::vector<std::string>()) {
auto bidder_worklet = CreateWorklet();
ASSERT_TRUE(bidder_worklet);
base::RunLoop run_loop;
RunReportWinExpectingResultAsync(bidder_worklet.get(), expected_report_url,
expected_ad_beacon_map,
std::move(expected_pa_requests),
expected_errors, run_loop.QuitClosure());
run_loop.Run();
}
// Creates a BidderWorkletNonSharedParams based on test fixture
// configuration.
mojom::BidderWorkletNonSharedParamsPtr CreateBidderWorkletNonSharedParams() {
std::vector<std::pair<auction_worklet::mojom::KAnonKeyPtr, bool>>
kanon_keys;
for (const auto& key : kanon_keys_) {
kanon_keys.emplace_back(key.first.Clone(), key.second);
}
return mojom::BidderWorkletNonSharedParams::New(
interest_group_name_,
interest_group_enable_bidding_signals_prioritization_,
interest_group_priority_vector_, execution_mode_, update_url_,
interest_group_trusted_bidding_signals_keys_,
interest_group_user_bidding_signals_, interest_group_ads_,
interest_group_ad_components_, std::move(kanon_keys));
}
// Creates a BiddingBrowserSignals based on test fixture configuration.
mojom::BiddingBrowserSignalsPtr CreateBiddingBrowserSignals() {
return mojom::BiddingBrowserSignals::New(
browser_signal_join_count_, browser_signal_bid_count_,
CloneWinList(browser_signal_prev_wins_));
}
// Create a BidderWorklet, returning the remote. If `out_bidder_worklet_impl`
// is non-null, will also stash the actual implementation pointer there.
// if `url` is empty, uses `interest_group_bidding_url_`.
mojo::Remote<mojom::BidderWorklet> CreateWorklet(
GURL url = GURL(),
bool pause_for_debugger_on_start = false,
BidderWorklet** out_bidder_worklet_impl = nullptr,
bool use_alternate_url_loader_factory = false) {
CHECK(!load_script_run_loop_);
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
if (use_alternate_url_loader_factory) {
alternate_url_loader_factory_.Clone(
url_loader_factory.InitWithNewPipeAndPassReceiver());
} else {
url_loader_factory_.Clone(
url_loader_factory.InitWithNewPipeAndPassReceiver());
}
auto bidder_worklet_impl = std::make_unique<BidderWorklet>(
v8_helper_, std::move(shared_storage_host_remote_),
pause_for_debugger_on_start, std::move(url_loader_factory),
url.is_empty() ? interest_group_bidding_url_ : url,
interest_group_wasm_url_, interest_group_trusted_bidding_signals_url_,
top_window_origin_, permissions_policy_state_.Clone(),
experiment_group_id_);
auto* bidder_worklet_ptr = bidder_worklet_impl.get();
mojo::Remote<mojom::BidderWorklet> bidder_worklet;
mojo::ReceiverId receiver_id =
bidder_worklets_.Add(std::move(bidder_worklet_impl),
bidder_worklet.BindNewPipeAndPassReceiver());
bidder_worklet_ptr->set_close_pipe_callback(
base::BindOnce(&BidderWorkletTest::ClosePipeCallback,
base::Unretained(this), receiver_id));
bidder_worklet.set_disconnect_with_reason_handler(base::BindRepeating(
&BidderWorkletTest::OnDisconnectWithReason, base::Unretained(this)));
if (out_bidder_worklet_impl) {
*out_bidder_worklet_impl = bidder_worklet_ptr;
}
return bidder_worklet;
}
// If no `generate_bid_client` is provided, uses one that invokes
// GenerateBidCallback().
void BeginGenerateBid(
mojom::BidderWorklet* bidder_worklet,
mojo::PendingAssociatedReceiver<mojom::GenerateBidFinalizer> finalizer,
mojo::PendingAssociatedRemote<mojom::GenerateBidClient>
generate_bid_client = mojo::NullAssociatedRemote()) {
if (!generate_bid_client) {
generate_bid_client =
GenerateBidClientWithCallbacks::Create(base::BindOnce(
&BidderWorkletTest::GenerateBidCallback, base::Unretained(this)));
}
bidder_worklet->BeginGenerateBid(
CreateBidderWorkletNonSharedParams(), kanon_mode_, join_origin_,
provide_direct_from_seller_signals_late_
? absl::nullopt
: direct_from_seller_per_buyer_signals_,
provide_direct_from_seller_signals_late_
? absl::nullopt
: direct_from_seller_auction_signals_,
browser_signal_seller_origin_, browser_signal_top_level_seller_origin_,
CreateBiddingBrowserSignals(), auction_start_time_,
/*trace_id=*/1, std::move(generate_bid_client), std::move(finalizer));
bidder_worklet->SendPendingSignalsRequests();
}
// If no `generate_bid_client` is provided, uses one that invokes
// GenerateBidCallback().
void GenerateBid(mojom::BidderWorklet* bidder_worklet,
mojo::PendingAssociatedRemote<mojom::GenerateBidClient>
generate_bid_client = mojo::NullAssociatedRemote()) {
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
BeginGenerateBid(bidder_worklet,
bid_finalizer.BindNewEndpointAndPassReceiver(),
std::move(generate_bid_client));
bid_finalizer->FinishGenerateBid(auction_signals_, per_buyer_signals_,
per_buyer_timeout_, per_buyer_currency_,
provide_direct_from_seller_signals_late_
? direct_from_seller_per_buyer_signals_
: absl::nullopt,
provide_direct_from_seller_signals_late_
? direct_from_seller_auction_signals_
: absl::nullopt);
}
// Calls BeginGenerateBid()/FinishGenerateBid(), expecting the
// GenerateBidClient's OnGenerateBidComplete() method never to be invoked.
void GenerateBidExpectingNeverCompletes(
mojom::BidderWorklet* bidder_worklet) {
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
bidder_worklet->BeginGenerateBid(
CreateBidderWorkletNonSharedParams(), kanon_mode_, join_origin_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(),
auction_start_time_,
/*trace_id=*/1, GenerateBidClientWithCallbacks::CreateNeverCompletes(),
bid_finalizer.BindNewEndpointAndPassReceiver());
bidder_worklet->SendPendingSignalsRequests();
bid_finalizer->FinishGenerateBid(
auction_signals_, per_buyer_signals_, per_buyer_timeout_,
per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
}
// Create a BidderWorklet and invokes BeginGenerateBid()/FinishGenerateBid(),
// waiting for the GenerateBid() callback to be invoked. Returns a null
// Remote on failure.
mojo::Remote<mojom::BidderWorklet> CreateWorkletAndGenerateBid() {
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
load_script_run_loop_.reset();
if (!bid_) {
return mojo::Remote<mojom::BidderWorklet>();
}
return bidder_worklet;
}
void GenerateBidCallback(
mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version,
bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority,
bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) {
absl::optional<uint32_t> maybe_data_version;
if (has_data_version) {
maybe_data_version = data_version;
}
absl::optional<double> maybe_set_priority;
if (has_set_priority) {
maybe_set_priority = set_priority;
}
bid_ = std::move(bid);
kanon_bid_ = std::move(kanon_bid);
data_version_ = maybe_data_version;
bid_debug_loss_report_url_ = debug_loss_report_url;
bid_debug_win_report_url_ = debug_win_report_url;
set_priority_ = maybe_set_priority;
update_priority_signals_overrides_.clear();
for (const auto& override : update_priority_signals_overrides) {
absl::optional<double> value;
if (override.second) {
value = override.second->value;
}
update_priority_signals_overrides_.emplace(override.first, value);
}
pa_requests_ = std::move(pa_requests);
non_kanon_pa_requests_ = std::move(non_kanon_pa_requests);
generate_bid_dependency_latencies_ =
std::move(generate_bid_dependency_latencies);
reject_reason_ = reject_reason;
if (bid_) {
// Shouldn't have a reject reason if the bid isn't rejected.
EXPECT_EQ(reject_reason_, mojom::RejectReason::kNotAvailable);
}
bid_errors_ = errors;
load_script_run_loop_->Quit();
}
// Waits for OnDisconnectWithReason() to be invoked, if it hasn't been
// already, and returns the error string it was invoked with.
std::string WaitForDisconnect() {
DCHECK(!disconnect_run_loop_);
if (!disconnect_reason_) {
disconnect_run_loop_ = std::make_unique<base::RunLoop>();
disconnect_run_loop_->Run();
disconnect_run_loop_.reset();
}
DCHECK(disconnect_reason_);
std::string disconnect_reason = std::move(disconnect_reason_).value();
disconnect_reason_.reset();
return disconnect_reason;
}
protected:
void ClosePipeCallback(mojo::ReceiverId receiver_id,
const std::string& description) {
bidder_worklets_.RemoveWithReason(receiver_id, /*custom_reason_code=*/0,
description);
}
void OnDisconnectWithReason(uint32_t custom_reason,
const std::string& description) {
DCHECK(!disconnect_reason_);
disconnect_reason_ = description;
if (disconnect_run_loop_) {
disconnect_run_loop_->Quit();
}
}
std::vector<mojo::StructPtr<mojom::PreviousWin>> CloneWinList(
const std::vector<mojo::StructPtr<mojom::PreviousWin>>& prev_win_list) {
std::vector<mojo::StructPtr<mojom::PreviousWin>> out;
for (const auto& prev_win : prev_win_list) {
out.push_back(prev_win->Clone());
}
return out;
}
TaskEnvironment task_environment_;
// Values used to construct the BiddingInterestGroup passed to the
// BidderWorklet.
//
// NOTE: For each new GURL field, GenerateBidLoadCompletionOrder /
// ReportWinLoadCompletionOrder should be updated.
std::string interest_group_name_;
bool interest_group_enable_bidding_signals_prioritization_;
absl::optional<base::flat_map<std::string, double>>
interest_group_priority_vector_;
GURL interest_group_bidding_url_ = GURL("https://url.test/");
url::Origin join_origin_;
blink::mojom::InterestGroup::ExecutionMode execution_mode_ =
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode;
absl::optional<GURL> interest_group_wasm_url_;
absl::optional<std::string> interest_group_user_bidding_signals_;
std::vector<blink::InterestGroup::Ad> interest_group_ads_;
absl::optional<std::vector<blink::InterestGroup::Ad>>
interest_group_ad_components_;
base::flat_map<auction_worklet::mojom::KAnonKeyPtr, bool> kanon_keys_;
auction_worklet::mojom::KAnonymityBidMode kanon_mode_ =
auction_worklet::mojom::KAnonymityBidMode::kNone;
absl::optional<GURL> update_url_;
absl::optional<GURL> interest_group_trusted_bidding_signals_url_;
absl::optional<std::vector<std::string>>
interest_group_trusted_bidding_signals_keys_;
int browser_signal_join_count_;
int browser_signal_bid_count_;
std::vector<mojo::StructPtr<mojom::PreviousWin>> browser_signal_prev_wins_;
absl::optional<std::string> auction_signals_;
absl::optional<std::string> per_buyer_signals_;
absl::optional<GURL> direct_from_seller_per_buyer_signals_;
absl::optional<GURL> direct_from_seller_auction_signals_;
absl::optional<base::TimeDelta> per_buyer_timeout_;
absl::optional<blink::AdCurrency> per_buyer_currency_;
url::Origin top_window_origin_;
mojom::AuctionWorkletPermissionsPolicyStatePtr permissions_policy_state_;
absl::optional<uint16_t> experiment_group_id_;
url::Origin browser_signal_seller_origin_;
absl::optional<url::Origin> browser_signal_top_level_seller_origin_;
bool provide_direct_from_seller_signals_late_ = false;
std::string seller_signals_;
// Used for both the output GenerateBid(), and the input of ReportWin().
absl::optional<uint32_t> data_version_;
GURL browser_signal_render_url_;
double browser_signal_bid_;
absl::optional<blink::AdCurrency> browser_signal_bid_currency_;
double browser_signal_highest_scoring_other_bid_;
absl::optional<blink::AdCurrency>
browser_signal_highest_scoring_other_bid_currency_;
bool browser_signal_made_highest_scoring_other_bid_;
absl::optional<double> browser_signal_ad_cost_;
absl::optional<uint16_t> browser_signal_modeling_signals_;
uint8_t browser_signal_recency_;
// Used for reportWin();
auction_worklet::mojom::ReportingIdField reporting_id_field_ =
auction_worklet::mojom::ReportingIdField::kInterestGroupName;
std::string reporting_id_;
// Use a single constant start time. Only delta times are provided to scripts,
// relative to the time of the auction, so no need to vary the auction time.
const base::Time auction_start_time_ = base::Time::Now();
// Reuseable run loop for loading the script. It's always populated after
// creating the worklet, to cause a crash if the callback is invoked
// synchronously.
std::unique_ptr<base::RunLoop> load_script_run_loop_;
// Values passed to the GenerateBidCallback().
mojom::BidderWorkletBidPtr bid_;
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid_;
absl::optional<GURL> bid_debug_loss_report_url_;
absl::optional<GURL> bid_debug_win_report_url_;
absl::optional<double> set_priority_;
// Uses absl::optional<double> instead of the Mojo type to be more
// user-friendly.
base::flat_map<std::string, absl::optional<double>>
update_priority_signals_overrides_;
PrivateAggregationRequests pa_requests_;
PrivateAggregationRequests non_kanon_pa_requests_;
mojom::GenerateBidDependencyLatenciesPtr generate_bid_dependency_latencies_;
mojom::RejectReason reject_reason_ = mojom::RejectReason::kNotAvailable;
std::vector<std::string> bid_errors_;
network::TestURLLoaderFactory url_loader_factory_;
network::TestURLLoaderFactory alternate_url_loader_factory_;
scoped_refptr<AuctionV8Helper> v8_helper_;
mojo::PendingRemote<mojom::AuctionSharedStorageHost>
shared_storage_host_remote_;
// Reuseable run loop for disconnection errors.
std::unique_ptr<base::RunLoop> disconnect_run_loop_;
absl::optional<std::string> disconnect_reason_;
// Owns all created BidderWorklets - having a ReceiverSet allows them to have
// a ClosePipeCallback which behaves just like the one in
// AuctionWorkletServiceImpl, to better match production behavior.
mojo::UniqueReceiverSet<mojom::BidderWorklet> bidder_worklets_;
};
// Test the case the BidderWorklet pipe is closed before invoking the
// GenerateBidCallback. The invocation of the GenerateBidCallback is not
// observed, since the callback is on the pipe that was just closed. There
// should be no Mojo exception due to destroying the creation callback without
// invoking it.
TEST_F(BidderWorkletTest, PipeClosed) {
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingNeverCompletes(bidder_worklet.get());
bidder_worklet.reset();
EXPECT_FALSE(bidder_worklets_.empty());
// This should not result in a Mojo crash.
task_environment_.RunUntilIdle();
EXPECT_TRUE(bidder_worklets_.empty());
}
TEST_F(BidderWorkletTest, NetworkError) {
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
CreateBasicGenerateBidScript(),
net::HTTP_NOT_FOUND);
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingNeverCompletes(bidder_worklet.get());
EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
WaitForDisconnect());
}
TEST_F(BidderWorkletTest, CompileError) {
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
"Invalid Javascript");
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingNeverCompletes(bidder_worklet.get());
std::string error = WaitForDisconnect();
EXPECT_THAT(error, StartsWith("https://url.test/:1 "));
EXPECT_THAT(error, HasSubstr("SyntaxError"));
}
TEST_F(BidderWorkletTest, GenerateBidReturnValueAd) {
// Missing `ad` field should be treated as null.
RunGenerateBidWithReturnValueExpectingResult(
R"({bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Explicitly setting an undefined ad value acts just like not setting an ad
// value.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: globalThis.not_defined, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Make sure "ad" can be of a variety of JS object types.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"(["ad"])", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: {a:1,b:null}, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"({"a":1,"b":null})", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: [2.5,[]], bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"[2.5,[]]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: -5, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"-5", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Some values that can't be represented in JSON become null.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: 0/0, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: [globalThis.not_defined], bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"[null]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: [function() {return 1;}], bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"[null]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Other values JSON can't represent result in failing instead of null.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: function() {return 1;}, bid:1, render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid has invalid ad value."});
// Make sure recursive structures aren't allowed in ad field.
RunGenerateBidWithJavascriptExpectingResult(
R"(
function generateBid() {
var a = [];
a[0] = a;
return {ad: a, bid:1, render:"https://response.test/"};
}
)",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid has invalid ad value."});
}
TEST_F(BidderWorkletTest, GenerateBidReturnValueBid) {
// Undefined / an empty return statement and null are treated as not bidding.
RunGenerateBidWithReturnValueExpectingResult(
"",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
RunGenerateBidWithReturnValueExpectingResult(
"null",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
// Missing bid value.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() returned object must have numeric bid "
"field."});
// Valid positive bid values.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1.5, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1.5, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:2, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 2, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:0.001, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 0.001, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Bids <= 0.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:0, render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:-10, render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:-1.5, render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
// Infinite and NaN bid.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1/0, render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid of inf is not a valid bid."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:-1/0, render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid of -inf is not a valid bid."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:0/0, render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid of nan is not a valid bid."});
// Non-numeric bid.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:"1", render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() returned object must have numeric bid "
"field."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:[1], render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() returned object must have numeric bid "
"field."});
}
TEST_F(BidderWorkletTest, GenerateBidReturnBidCurrencyExpectUnspecified) {
// Tests of returning various currency and comparing against unspecifed
// expectation.
// Explicitly specified correct one.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1, bidCurrency: "USD",
render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, blink::AdCurrency::From("USD"),
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(reject_reason_, mojom::RejectReason::kNotAvailable);
// Not specifying currency explicitly results in nullopt tag.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1,
render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(reject_reason_, mojom::RejectReason::kNotAvailable);
// Trying to explicitly specify kUnspecifiedAdCurrency fails.
RunGenerateBidWithReturnValueExpectingResult(
base::StringPrintf(
R"({ad: "ad", bid:1, bidCurrency:"%s",
render:"https://response.test/"})",
blink::kUnspecifiedAdCurrency),
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bidCurrency of '???' "
"is not a currency code."});
EXPECT_EQ(reject_reason_, mojom::RejectReason::kWrongGenerateBidCurrency);
// Expect currency codes to be 3 characters.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1, bidCurrency:"USSD",
render:"https://response.test/"})",
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bidCurrency of 'USSD' "
"is not a currency code."});
EXPECT_EQ(reject_reason_, mojom::RejectReason::kWrongGenerateBidCurrency);
// Expect currency code to be 3 uppercase characters.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1, bidCurrency:"usd",
render:"https://response.test/"})",
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bidCurrency of 'usd' "
"is not a currency code."});
EXPECT_EQ(reject_reason_, mojom::RejectReason::kWrongGenerateBidCurrency);
}
TEST_F(BidderWorkletTest, GenerateBidReturnBidCurrencyExpectCAD) {
// Tests of returning various currency and comparing against expected `CAD`.
//
per_buyer_currency_ = blink::AdCurrency::From("CAD");
// Explicitly specified correct one.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1, bidCurrency: "CAD",
render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, blink::AdCurrency::From("CAD"),
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(reject_reason_, mojom::RejectReason::kNotAvailable);
// Explicitly specified incorrect one.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1, bidCurrency: "USD",
render:"https://response.test/"})",
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bidCurrency mismatch; "
"returned 'USD', expected 'CAD'."});
EXPECT_EQ(reject_reason_, mojom::RejectReason::kWrongGenerateBidCurrency);
// Not specifying currency explicitly results in absl::nullopt, which matches
// for compatibility reasons.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1,
render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(reject_reason_, mojom::RejectReason::kNotAvailable);
}
TEST_F(BidderWorkletTest, GenerateBidReturnValueUrl) {
// Missing render field.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid has incorrect structure."});
// Missing ad and render fields.
RunGenerateBidWithReturnValueExpectingResult(
R"({bid:1})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid has incorrect structure."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Missing value with bid <= 0 is considered a valid no-bid case.
RunGenerateBidWithReturnValueExpectingResult(
R"({bid:0})",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
RunGenerateBidWithReturnValueExpectingResult(
R"({bid:-1})",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
// Disallowed render schemes.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:"http://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid render URL 'http://response.test/' "
"isn't a valid https:// URL."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:"chrome-extension://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid render URL "
"'chrome-extension://response.test/' isn't a valid https:// URL."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:"about:blank"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid render URL 'about:blank' isn't a "
"valid https:// URL."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:"data:,foo"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid render URL 'data:,foo' isn't a "
"valid https:// URL."});
// Invalid render URLs.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:"test"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid render URL '' isn't a valid "
"https:// URL."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:"http://"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid render URL 'http:' isn't a valid "
"https:// URL."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:["http://response.test/"]})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid has incorrect structure."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: ["ad"], bid:1, render:9})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid has incorrect structure."});
}
TEST_F(BidderWorkletTest, GenerateBidReturnValueAdComponents) {
// ----------------------
// No adComponents in IG.
// ----------------------
interest_group_ad_components_ = absl::nullopt;
// Auction should fail if adComponents in return value is an array.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:["http://response.test/"]})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid contains adComponents but "
"InterestGroup has no adComponents."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:[]})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid contains adComponents but "
"InterestGroup has no adComponents."});
// Auction should fail if adComponents in return value is an unexpected type.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:5})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid contains adComponents but "
"InterestGroup has no adComponents."});
// Not present and null adComponents fields should result in success.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:null})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
SetDefaultParameters();
// -----------------------------
// Non-empty adComponents in IG.
// -----------------------------
// Empty adComponents in results is allowed.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Auction should fail if adComponents in return value is an unexpected type.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:5})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid adComponents value must be an "
"array."});
// Unexpected value types in adComponents should fail.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:[{}]})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid adComponents value must be an "
"array of strings or objects that contain the url string field and "
"optional width and height fields."});
// Up to 20 values in the output adComponents output array are allowed (And
// they can all be the same URL).
static_assert(blink::kMaxAdAuctionAdComponents == 20,
"Unexpected value of kMaxAdAuctionAdComponents");
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:[
"https://ad_component.test/" /* 1 */,
"https://ad_component.test/" /* 2 */,
"https://ad_component.test/" /* 3 */,
"https://ad_component.test/" /* 4 */,
"https://ad_component.test/" /* 5 */,
"https://ad_component.test/" /* 6 */,
"https://ad_component.test/" /* 7 */,
"https://ad_component.test/" /* 8 */,
"https://ad_component.test/" /* 9 */,
"https://ad_component.test/" /* 10 */,
"https://ad_component.test/" /* 11 */,
"https://ad_component.test/" /* 12 */,
"https://ad_component.test/" /* 13 */,
"https://ad_component.test/" /* 14 */,
"https://ad_component.test/" /* 15 */,
"https://ad_component.test/" /* 16 */,
"https://ad_component.test/" /* 17 */,
"https://ad_component.test/" /* 18 */,
"https://ad_component.test/" /* 19 */,
"https://ad_component.test/" /* 20 */,
]})",
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/
std::vector<blink::AdDescriptor>{
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 1 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 2 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 3 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 4 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 5 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 6 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 7 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 8 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 9 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 10 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 11 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 12 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 13 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 14 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 15 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 16 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 17 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 18 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 19 */,
blink::AdDescriptor(GURL("https://ad_component.test/")) /* 20 */,
},
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Results with 21 or more values are rejected.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:[
"https://ad_component.test/" /* 1 */,
"https://ad_component.test/" /* 2 */,
"https://ad_component.test/" /* 3 */,
"https://ad_component.test/" /* 4 */,
"https://ad_component.test/" /* 5 */,
"https://ad_component.test/" /* 6 */,
"https://ad_component.test/" /* 7 */,
"https://ad_component.test/" /* 8 */,
"https://ad_component.test/" /* 9 */,
"https://ad_component.test/" /* 10 */,
"https://ad_component.test/" /* 11 */,
"https://ad_component.test/" /* 12 */,
"https://ad_component.test/" /* 13 */,
"https://ad_component.test/" /* 14 */,
"https://ad_component.test/" /* 15 */,
"https://ad_component.test/" /* 16 */,
"https://ad_component.test/" /* 17 */,
"https://ad_component.test/" /* 18 */,
"https://ad_component.test/" /* 19 */,
"https://ad_component.test/" /* 20 */,
"https://ad_component.test/" /* 21 */,
]})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid adComponents with over 20 "
"items."});
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignals) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: 123})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/123u, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsNaN) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: NaN})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsInfinity) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: Infinity})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsNegative) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: -1})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsNegativeInfinity) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: -Infinity})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsNegativeZero) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: -0})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsPositiveZero) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: 0})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/0u, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsNonInteger) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: 123.5})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/123u, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsTooLarge) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: 4096})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidModelingSignalsAlmostTooLarge) {
const std::string kGenerateBidBody =
R"({bid:1, render:"https://response.test/", modelingSignals: 4095.9})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/4095, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidReturnValueInvalid) {
// Valid JS, but missing function.
RunGenerateBidWithJavascriptExpectingResult(
R"(
function someOtherFunction() {
return {ad: ["ad"], bid:1, render:"https://response.test/"};
}
)",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ `generateBid` is not a function."});
RunGenerateBidWithJavascriptExpectingResult(
"", /*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ `generateBid` is not a function."});
RunGenerateBidWithJavascriptExpectingResult(
"5", /*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ `generateBid` is not a function."});
// Throw exception.
RunGenerateBidWithJavascriptExpectingResult(
"shrimp", /*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:1 Uncaught ReferenceError: "
"shrimp is not defined."});
}
// Test parsing of setBid arguments.
TEST_F(BidderWorkletTest, GenerateBidSetBidThrows) {
// --------
// Vary ad
// --------
// Other values JSON can't represent result in failing instead of null.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: function() {return 1;}, bid:1, render:"https://response.test/"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid has invalid ad value."});
// Make sure recursive structures aren't allowed in ad field.
RunGenerateBidWithJavascriptExpectingResult(
R"(
function generateBid() {
var a = [];
a[0] = a;
setBid({ad: a, bid:1, render:"https://response.test/"});
return {};
}
)",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:5 Uncaught TypeError: bid has invalid ad value."});
// --------
// Vary bid
// --------
// Non-numeric bid.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:"1", render:"https://response.test/"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: returned object must have "
"numeric bid field."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:[1], render:"https://response.test/"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: returned object must have "
"numeric bid field."});
// ---------
// Vary URL.
// ---------
// Disallowed render schemes.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:1, render:"http://response.test/"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid render URL "
"'http://response.test/' isn't a valid https:// URL."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"],
bid:1,
render:"chrome-extension://response.test/"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid render URL "
"'chrome-extension://response.test/' isn't a valid https:// URL."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:1, render:"about:blank"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid render URL 'about:blank' "
"isn't a valid https:// URL."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:1, render:"data:,foo"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid render URL 'data:,foo' "
"isn't a valid https:// URL."});
// Invalid render URLs.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:1, render:"test"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid render URL '' isn't a "
"valid https:// URL."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:1, render:"http://"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid render URL 'http:' isn't a "
"valid https:// URL."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:1, render:["http://response.test/"]});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid has incorrect structure."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:1, render:9});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid has incorrect structure."});
// ----------------------
// No adComponents in IG.
// ----------------------
interest_group_ad_components_ = absl::nullopt;
// Auction should fail if adComponents in return value is an array.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:["http://response.test/"]});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid contains adComponents but "
"InterestGroup has no adComponents."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:[]});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid contains adComponents but "
"InterestGroup has no adComponents."});
// Auction should fail if adComponents in return value is an unexpected type.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:5});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid contains adComponents but "
"InterestGroup has no adComponents."});
SetDefaultParameters();
// -----------------------------
// Non-empty adComponents in IG.
// -----------------------------
// Auction should fail if adComponents in return value is an unexpected type.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:5});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid adComponents value must be "
"an array."});
// Unexpected value types in adComponents should fail.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:[{}]});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid adComponents value must be "
"an array of strings or objects that contain the url string field and "
"optional width and height fields."});
// Up to 20 values in the output adComponents output array are allowed (And
// they can all be the same URL).
static_assert(blink::kMaxAdAuctionAdComponents == 20,
"Unexpected value of kMaxAdAuctionAdComponents");
// Results with 21 or more values are rejected.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: "ad",
bid:1,
render:"https://response.test/",
adComponents:[
"https://ad_component.test/" /* 1 */,
"https://ad_component.test/" /* 2 */,
"https://ad_component.test/" /* 3 */,
"https://ad_component.test/" /* 4 */,
"https://ad_component.test/" /* 5 */,
"https://ad_component.test/" /* 6 */,
"https://ad_component.test/" /* 7 */,
"https://ad_component.test/" /* 8 */,
"https://ad_component.test/" /* 9 */,
"https://ad_component.test/" /* 10 */,
"https://ad_component.test/" /* 11 */,
"https://ad_component.test/" /* 12 */,
"https://ad_component.test/" /* 13 */,
"https://ad_component.test/" /* 14 */,
"https://ad_component.test/" /* 15 */,
"https://ad_component.test/" /* 16 */,
"https://ad_component.test/" /* 17 */,
"https://ad_component.test/" /* 18 */,
"https://ad_component.test/" /* 19 */,
"https://ad_component.test/" /* 20 */,
"https://ad_component.test/" /* 21 */,
]});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid adComponents with over 20 "
"items."});
// ------------
// Other cases.
// ------------
// No return value.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid(1);
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: bid not an object."});
// Missing value.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({bid:"a", render:"https://response.test/"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: returned object must have "
"numeric bid field."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], render:"https://response.test/"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: returned object must have "
"numeric bid field."});
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({ad: ["ad"], bid:"a"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:2 Uncaught TypeError: returned object must have "
"numeric bid field."});
// Setting a valid bid with setBid(), followed by setting an invalid bid that
// throws, should result in no bid being set.
RunGenerateBidWithJavascriptExpectingResult(
R"(function generateBid() {
setBid({bid:1, render:"https://response.test/"});
setBid({ad: ["ad"], bid:"1", render:"https://response.test/"});
return {ad: "not_reached", bid: 4, render:"https://response.test/2"};
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:3 Uncaught TypeError: returned object must have "
"numeric bid field."});
}
// Make sure Date() is not available when running generateBid().
TEST_F(BidderWorkletTest, GenerateBidDateNotAvailable) {
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: Date().toString(), bid:1, render:"https://response.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught ReferenceError: Date is not defined."});
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupOwner) {
interest_group_bidding_url_ = GURL("https://foo.test/bar");
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: interestGroup.owner, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"("https://foo.test")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_bidding_url_ = GURL("https://[::1]:40000/");
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: interestGroup.owner, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"("https://[::1]:40000")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupName) {
const std::string kGenerateBidBody =
R"({ad: interestGroup.name, bid:1, render:"https://response.test/"})";
interest_group_name_ = "foo";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("foo")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_name_ = R"("foo")";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("\"foo\"")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_name_ = "[1]";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("[1]")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest,
GenerateBidInterestGroupUseBiddingSignalsPrioritization) {
RunGenerateBidExpectingExpressionIsTrue(
"interestGroup.useBiddingSignalsPrioritization === false");
interest_group_enable_bidding_signals_prioritization_ = true;
RunGenerateBidExpectingExpressionIsTrue(
"interestGroup.useBiddingSignalsPrioritization === true");
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupPriorityVector) {
RunGenerateBidExpectingExpressionIsTrue(
"interestGroup.priorityVector === undefined");
interest_group_priority_vector_.emplace();
RunGenerateBidExpectingExpressionIsTrue(
"Object.keys(interestGroup.priorityVector).length === 0");
interest_group_priority_vector_->emplace("foo", 4);
RunGenerateBidExpectingExpressionIsTrue(
"Object.keys(interestGroup.priorityVector).length === 1");
RunGenerateBidExpectingExpressionIsTrue(
"interestGroup.priorityVector['foo'] === 4");
interest_group_priority_vector_->emplace("bar", -5);
RunGenerateBidExpectingExpressionIsTrue(
"Object.keys(interestGroup.priorityVector).length === 2");
RunGenerateBidExpectingExpressionIsTrue(
"interestGroup.priorityVector['foo'] === 4");
RunGenerateBidExpectingExpressionIsTrue(
"interestGroup.priorityVector['bar'] === -5");
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupBiddingLogicUrl) {
const std::string kGenerateBidBody =
R"({ad: interestGroup.biddingLogicUrl, bid:1,
render:"https://response.test/"})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("https://url.test/")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_bidding_url_ = GURL("https://url.test/foo");
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("https://url.test/foo")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupBiddingWasmHelperUrl) {
const std::string kGenerateBidBody =
R"({ad: "biddingWasmHelperUrl" in interestGroup ?
interestGroup.biddingWasmHelperUrl : "missing",
bid:1,
render:"https://response.test/"})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("missing")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_wasm_url_ = GURL(kWasmUrl);
AddResponse(&url_loader_factory_, GURL(kWasmUrl), kWasmMimeType,
/*charset=*/absl::nullopt, ToyWasm());
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("https://foo.test/helper.wasm")", 1,
/*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupUpdateUrl) {
const std::string kGenerateBidBody =
R"({ad: "updateUrl" in interestGroup ?
interestGroup.updateUrl : "missing",
bid:1,
render:"https://response.test/"})";
// TODO(https://crbug.com/1420080): Remove this and tests that use it when
// removing support for the deprecated `dailyUpdateUrl` field, in favor of
// `updateUrl`.
const std::string kGenerateBidBodyUsingDeprecatedDailyUpdateUrl =
R"({ad: "dailyUpdateUrl" in interestGroup ?
interestGroup.updateUrl : "missing",
bid:1,
render:"https://response.test/"})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("missing")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBodyUsingDeprecatedDailyUpdateUrl,
mojom::BidderWorkletBid::New(
R"("missing")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
update_url_ = GURL("https://url.test/daily_update");
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("https://url.test/daily_update")", 1,
/*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBodyUsingDeprecatedDailyUpdateUrl,
mojom::BidderWorkletBid::New(
R"("https://url.test/daily_update")", 1,
/*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupTrustedBiddingSignalsUrl) {
const std::string kGenerateBidBody =
R"({ad: "trustedBiddingSignalsUrl" in interestGroup ?
interestGroup.trustedBiddingSignalsUrl : "missing",
bid:1,
render:"https://response.test/"})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("missing")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_trusted_bidding_signals_url_ =
GURL("https://signals.test/foo.json");
// Need trusted signals response for the next test case, to prevent it from
// hanging.
AddBidderJsonResponse(
&url_loader_factory_,
GURL(interest_group_trusted_bidding_signals_url_->spec() +
"?hostname=top.window.test&interestGroupNames=Fred"),
"{}");
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("https://signals.test/foo.json")", 1,
/*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupTrustedBiddingSignalsKeys) {
const std::string kGenerateBidBody =
R"({ad: "trustedBiddingSignalsKeys" in interestGroup ?
interestGroup.trustedBiddingSignalsKeys : "missing",
bid:1,
render:"https://response.test/"})";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("missing")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// 0-length but non-null key list.
interest_group_trusted_bidding_signals_keys_.emplace();
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"([])", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_trusted_bidding_signals_keys_->push_back("2");
interest_group_trusted_bidding_signals_keys_->push_back("1");
interest_group_trusted_bidding_signals_keys_->push_back("3");
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"(["2","1","3"])", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidInterestGroupUserBiddingSignals) {
const std::string kGenerateBidBody =
R"({ad: interestGroup.userBiddingSignals, bid:1, render:"https://response.test/"})";
// Since UserBiddingSignals are in JSON, non-JSON like standalone string
// literals should not come in, but we are required to hangle such cases
// gracefully, since it's ultimately data from the renderer. In this case it
// just turns into null.
interest_group_user_bidding_signals_ = "foo";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_user_bidding_signals_ = R"("foo")";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("foo")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_user_bidding_signals_ = "[1]";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"[1]", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
interest_group_user_bidding_signals_ = absl::nullopt;
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: interestGroup.userBiddingSignals === undefined, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"true", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
// Test multiple GenerateBid calls on a single worklet, in parallel. Do this
// twice, once before the worklet has loaded its Javascript, and once after, to
// make sure both cases work.
TEST_F(BidderWorkletTest, GenerateBidParallel) {
// Each GenerateBid call provides a different `auctionSignals` value. Use that
// in the result for testing.
const char kBidderScriptReturnValue[] = R"({
ad: auctionSignals,
bid: auctionSignals,
render:"https://response.test/"
})";
auto bidder_worklet = CreateWorklet();
// For the first loop iteration, call GenerateBid repeatedly and only then
// provide the bidder script. For the second loop iteration, reuse the bidder
// worklet from the first iteration, so the Javascript is loaded from the
// start.
for (bool generate_bid_invoked_before_worklet_script_loaded : {false, true}) {
SCOPED_TRACE(generate_bid_invoked_before_worklet_script_loaded);
base::RunLoop run_loop;
const size_t kNumGenerateBidCalls = 10;
size_t num_generate_bid_calls = 0;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
size_t bid_value = i + 1;
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
bidder_worklet->BeginGenerateBid(
CreateBidderWorkletNonSharedParams(), kanon_mode_, join_origin_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_,
CreateBiddingBrowserSignals(), auction_start_time_,
/*trace_id=*/1,
GenerateBidClientWithCallbacks::Create(base::BindLambdaForTesting(
[&run_loop, &num_generate_bid_calls, bid_value](
mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version, bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority, bool has_set_priority,
base::flat_map<
std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) {
EXPECT_EQ(bid_value, bid->bid);
EXPECT_EQ(base::NumberToString(bid_value), bid->ad);
EXPECT_EQ(GURL("https://response.test/"),
bid->ad_descriptor.url);
EXPECT_FALSE(kanon_bid);
EXPECT_FALSE(has_data_version);
EXPECT_TRUE(errors.empty());
++num_generate_bid_calls;
if (num_generate_bid_calls == kNumGenerateBidCalls) {
run_loop.Quit();
}
})),
bid_finalizer.BindNewEndpointAndPassReceiver());
bid_finalizer->FinishGenerateBid(
/*auction_signals_json=*/base::NumberToString(bid_value),
per_buyer_signals_, per_buyer_timeout_, per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
}
// If this is the first loop iteration, wait for all the Mojo calls to
// settle, and then provide the Javascript response body.
if (generate_bid_invoked_before_worklet_script_loaded == false) {
// Since the script hasn't loaded yet, no bids should be generated.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// Load script.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(kBidderScriptReturnValue));
}
run_loop.Run();
EXPECT_EQ(kNumGenerateBidCalls, num_generate_bid_calls);
}
}
// Test multiple GenerateBid calls on a single worklet, in parallel, in the case
// the script fails to load.
TEST_F(BidderWorkletTest, GenerateBidParallelLoadFails) {
auto bidder_worklet = CreateWorklet();
for (size_t i = 0; i < 10; ++i) {
GenerateBidExpectingNeverCompletes(bidder_worklet.get());
}
// Script fails to load.
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
CreateBasicGenerateBidScript(),
net::HTTP_NOT_FOUND);
EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
WaitForDisconnect());
}
// Test multiple GenerateBid calls on a single worklet, in parallel, in the case
// there are trusted bidding signals.
//
// In this test, the ordering is:
// 1) GenerateBid() calls are made.
// 2) The worklet script load completes.
// 3) The trusted bidding signals are loaded.
TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignalsParallelBatched1) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
interest_group_trusted_bidding_signals_keys_.emplace();
// Each GenerateBid() call provides a different `auctionSignals` value. The
// `i` generateBid() call should have trusted scoring signals of {"i":i+1},
// and this function uses the key as the "ad" parameter, and the value as the
// bid.
const char kBidderScriptReturnValue[] = R"({
ad: Number(Object.keys(trustedBiddingSignals)[0]),
bid: trustedBiddingSignals[Object.keys(trustedBiddingSignals)[0]],
render:"https://response.test/"
})";
auto bidder_worklet = CreateWorklet();
// 1) GenerateBid() calls are made.
base::RunLoop run_loop;
const size_t kNumGenerateBidCalls = 10;
size_t num_generate_bid_calls = 0;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
// Append a different key for each request.
auto interest_group_fields = CreateBidderWorkletNonSharedParams();
interest_group_fields->trusted_bidding_signals_keys->push_back(
base::NumberToString(i));
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
bidder_worklet->BeginGenerateBid(
std::move(interest_group_fields), kanon_mode_, join_origin_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(),
auction_start_time_,
/*trace_id=*/1,
GenerateBidClientWithCallbacks::Create(base::BindLambdaForTesting(
[&run_loop, &num_generate_bid_calls, i](
mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version, bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority, bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) {
EXPECT_EQ(base::NumberToString(i), bid->ad);
EXPECT_EQ(i + 1, bid->bid);
EXPECT_EQ(GURL("https://response.test/"), bid->ad_descriptor.url);
EXPECT_FALSE(kanon_bid);
EXPECT_EQ(10u, data_version);
EXPECT_TRUE(has_data_version);
EXPECT_TRUE(errors.empty());
++num_generate_bid_calls;
if (num_generate_bid_calls == kNumGenerateBidCalls) {
run_loop.Quit();
}
})),
bid_finalizer.BindNewEndpointAndPassReceiver());
bid_finalizer->FinishGenerateBid(
auction_signals_, per_buyer_signals_, per_buyer_timeout_,
per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
}
// This should trigger a single network request for all needed signals.
bidder_worklet->SendPendingSignalsRequests();
// Calling GenerateBid() shouldn't cause any callbacks to be invoked - the
// BidderWorklet is waiting on both the trusted bidding signals and Javascript
// responses from the network.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// 2) The worklet script load completes.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(kBidderScriptReturnValue));
// No callbacks are invoked, as the BidderWorklet is still waiting on the
// trusted bidding signals responses.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// 3) The trusted bidding signals are loaded.
std::string keys;
base::Value::Dict keys_dict;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
if (i != 0) {
keys.append(",");
}
keys.append(base::NumberToString(i));
keys_dict.Set(base::NumberToString(i), static_cast<int>(i + 1));
}
base::Value::Dict signals_dict;
signals_dict.Set("keys", std::move(keys_dict));
std::string signals_json;
base::JSONWriter::Write(signals_dict, &signals_json);
AddBidderJsonResponse(
&url_loader_factory_,
GURL(base::StringPrintf(
"https://signals.test/"
"?hostname=top.window.test&keys=%s&interestGroupNames=Fred",
keys.c_str())),
signals_json, /*data_version=*/10u);
// The worklets can now generate bids.
run_loop.Run();
EXPECT_EQ(kNumGenerateBidCalls, num_generate_bid_calls);
}
// Test multiple GenerateBid calls on a single worklet, in parallel, in the case
// there are trusted bidding signals.
//
// In this test, the ordering is:
// 1) GenerateBid() calls are made
// 2) The trusted bidding signals are loaded.
// 3) The worklet script load completes.
TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignalsParallelBatched2) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
interest_group_trusted_bidding_signals_keys_.emplace();
// Each GenerateBid() call provides a different `auctionSignals` value. The
// `i` generateBid() call should have trusted scoring signals of {"i":i+1},
// and this function uses the key as the "ad" parameter, and the value as the
// bid.
const char kBidderScriptReturnValue[] = R"({
ad: Number(Object.keys(trustedBiddingSignals)[0]),
bid: trustedBiddingSignals[Object.keys(trustedBiddingSignals)[0]],
render:"https://response.test/"
})";
auto bidder_worklet = CreateWorklet();
// 1) GenerateBid() calls are made
base::RunLoop run_loop;
const size_t kNumGenerateBidCalls = 10;
size_t num_generate_bid_calls = 0;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
// Append a different key for each request.
auto interest_group_fields = CreateBidderWorkletNonSharedParams();
interest_group_fields->trusted_bidding_signals_keys->push_back(
base::NumberToString(i));
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
bidder_worklet->BeginGenerateBid(
std::move(interest_group_fields), kanon_mode_, join_origin_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(),
auction_start_time_,
/*trace_id=*/1,
GenerateBidClientWithCallbacks::Create(base::BindLambdaForTesting(
[&run_loop, &num_generate_bid_calls, i](
mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version, bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority, bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) {
EXPECT_EQ(base::NumberToString(i), bid->ad);
EXPECT_EQ(i + 1, bid->bid);
EXPECT_EQ(GURL("https://response.test/"), bid->ad_descriptor.url);
EXPECT_FALSE(kanon_bid);
EXPECT_EQ(42u, data_version);
EXPECT_TRUE(has_data_version);
EXPECT_TRUE(errors.empty());
++num_generate_bid_calls;
if (num_generate_bid_calls == kNumGenerateBidCalls) {
run_loop.Quit();
}
})),
bid_finalizer.BindNewEndpointAndPassReceiver());
bid_finalizer->FinishGenerateBid(
auction_signals_, per_buyer_signals_, per_buyer_timeout_,
per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
}
// This should trigger a single network request for all needed signals.
bidder_worklet->SendPendingSignalsRequests();
// Calling GenerateBid() shouldn't cause any callbacks to be invoked - the
// BidderWorklet is waiting on both the trusted bidding signals and Javascript
// responses from the network.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// 2) The trusted bidding signals are loaded.
std::string keys;
base::Value::Dict keys_dict;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
if (i != 0) {
keys.append(",");
}
keys.append(base::NumberToString(i));
keys_dict.Set(base::NumberToString(i), static_cast<int>(i + 1));
}
base::Value::Dict signals_dict;
signals_dict.Set("keys", std::move(keys_dict));
std::string signals_json;
base::JSONWriter::Write(signals_dict, &signals_json);
AddBidderJsonResponse(
&url_loader_factory_,
GURL(base::StringPrintf(
"https://signals.test/"
"?hostname=top.window.test&keys=%s&interestGroupNames=Fred",
keys.c_str())),
signals_json, /*data_version=*/42u);
// No callbacks should have been invoked, since the worklet script hasn't
// loaded yet.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// 3) The worklet script load completes.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(kBidderScriptReturnValue));
// The worklets can now generate bids.
run_loop.Run();
EXPECT_EQ(kNumGenerateBidCalls, num_generate_bid_calls);
}
// Test multiple GenerateBid calls on a single worklet, in parallel, in the case
// there are trusted bidding signals.
//
// In this test, the ordering is:
// 1) The worklet script load completes.
// 2) GenerateBid() calls are made.
// 3) The trusted bidding signals are loaded.
TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignalsParallelBatched3) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
interest_group_trusted_bidding_signals_keys_.emplace();
// Each GenerateBid() call provides a different `auctionSignals` value. The
// `i` generateBid() call should have trusted scoring signals of {"i":i+1},
// and this function uses the key as the "ad" parameter, and the value as the
// bid.
const char kBidderScriptReturnValue[] = R"({
ad: Number(Object.keys(trustedBiddingSignals)[0]),
bid: trustedBiddingSignals[Object.keys(trustedBiddingSignals)[0]],
render:"https://response.test/"
})";
auto bidder_worklet = CreateWorklet();
// 1) The worklet script load completes.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(kBidderScriptReturnValue));
task_environment_.RunUntilIdle();
// 2) GenerateBid() calls are made.
base::RunLoop run_loop;
const size_t kNumGenerateBidCalls = 10;
size_t num_generate_bid_calls = 0;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
// Append a different key for each request.
auto interest_group_fields = CreateBidderWorkletNonSharedParams();
interest_group_fields->trusted_bidding_signals_keys->push_back(
base::NumberToString(i));
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
bidder_worklet->BeginGenerateBid(
std::move(interest_group_fields), kanon_mode_, join_origin_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(),
auction_start_time_,
/*trace_id=*/1,
GenerateBidClientWithCallbacks::Create(base::BindLambdaForTesting(
[&run_loop, &num_generate_bid_calls, i](
mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version, bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority, bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) {
EXPECT_EQ(base::NumberToString(i), bid->ad);
EXPECT_EQ(i + 1, bid->bid);
EXPECT_FALSE(kanon_bid);
EXPECT_EQ(22u, data_version);
EXPECT_TRUE(has_data_version);
EXPECT_EQ(GURL("https://response.test/"), bid->ad_descriptor.url);
EXPECT_TRUE(errors.empty());
++num_generate_bid_calls;
if (num_generate_bid_calls == kNumGenerateBidCalls) {
run_loop.Quit();
}
})),
bid_finalizer.BindNewEndpointAndPassReceiver());
bid_finalizer->FinishGenerateBid(
auction_signals_, per_buyer_signals_, per_buyer_timeout_,
per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
}
// This should trigger a single network request for all needed signals.
bidder_worklet->SendPendingSignalsRequests();
// No callbacks should have been invoked yet, since the trusted bidding
// signals haven't loaded yet.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// 3) The trusted bidding signals are loaded.
std::string keys;
base::Value::Dict keys_dict;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
if (i != 0) {
keys.append(",");
}
keys.append(base::NumberToString(i));
keys_dict.Set(base::NumberToString(i), static_cast<int>(i + 1));
}
base::Value::Dict signals_dict;
signals_dict.Set("keys", std::move(keys_dict));
std::string signals_json;
base::JSONWriter::Write(signals_dict, &signals_json);
AddBidderJsonResponse(
&url_loader_factory_,
GURL(base::StringPrintf(
"https://signals.test/"
"?hostname=top.window.test&keys=%s&interestGroupNames=Fred",
keys.c_str())),
signals_json, /*data_version=*/22u);
// The worklets can now generate bids.
run_loop.Run();
EXPECT_EQ(num_generate_bid_calls, kNumGenerateBidCalls);
}
// Same as the first batched test, but without batching requests. No need to
// test all not batched order variations.
TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignalsParallelNotBatched) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
interest_group_trusted_bidding_signals_keys_.emplace();
// Each GenerateBid() call provides a different `auctionSignals` value. The
// `i` generateBid() call should have trusted scoring signals of {"i":i+1},
// and this function uses the key as the "ad" parameter, and the value as the
// bid.
const char kBidderScriptReturnValue[] = R"({
ad: Number(Object.keys(trustedBiddingSignals)[0]),
bid: trustedBiddingSignals[Object.keys(trustedBiddingSignals)[0]],
render:"https://response.test/"
})";
auto bidder_worklet = CreateWorklet();
// 1) BeginGenerateBid()/FinishGenerateBid() calls are made
base::RunLoop run_loop;
const size_t kNumGenerateBidCalls = 10;
size_t num_generate_bid_calls = 0;
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
// Append a different key for each request.
auto interest_group_fields = CreateBidderWorkletNonSharedParams();
interest_group_fields->trusted_bidding_signals_keys->push_back(
base::NumberToString(i));
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
bidder_worklet->BeginGenerateBid(
std::move(interest_group_fields), kanon_mode_, join_origin_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(),
auction_start_time_,
/*trace_id=*/1,
GenerateBidClientWithCallbacks::Create(base::BindLambdaForTesting(
[&run_loop, &num_generate_bid_calls, i](
mojom::BidderWorkletBidPtr bid,
mojom::BidderWorkletKAnonEnforcedBidPtr kanon_bid,
uint32_t data_version, bool has_data_version,
const absl::optional<GURL>& debug_loss_report_url,
const absl::optional<GURL>& debug_win_report_url,
double set_priority, bool has_set_priority,
base::flat_map<std::string,
auction_worklet::mojom::PrioritySignalsDoublePtr>
update_priority_signals_overrides,
PrivateAggregationRequests pa_requests,
PrivateAggregationRequests non_kanon_pa_requests,
base::TimeDelta bidding_latency,
mojom::GenerateBidDependencyLatenciesPtr
generate_bid_dependency_latencies,
mojom::RejectReason reject_reason,
const std::vector<std::string>& errors) {
EXPECT_EQ(base::NumberToString(i), bid->ad);
EXPECT_EQ(i + 1, bid->bid);
EXPECT_EQ(GURL("https://response.test/"), bid->ad_descriptor.url);
EXPECT_FALSE(kanon_bid);
EXPECT_EQ(i, data_version);
EXPECT_TRUE(has_data_version);
EXPECT_TRUE(errors.empty());
++num_generate_bid_calls;
if (num_generate_bid_calls == kNumGenerateBidCalls) {
run_loop.Quit();
}
})),
bid_finalizer.BindNewEndpointAndPassReceiver());
// Send one request at a time.
bidder_worklet->SendPendingSignalsRequests();
bid_finalizer->FinishGenerateBid(
auction_signals_, per_buyer_signals_, per_buyer_timeout_,
per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
}
// Calling FinishGenerateBid() shouldn't cause any callbacks to be invoked -
// the BidderWorklet is waiting on both the trusted bidding signals and
// Javascript responses from the network.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// 2) The worklet script load completes.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(kBidderScriptReturnValue));
// No callbacks are invoked, as the BidderWorklet is still waiting on the
// trusted bidding signals responses.
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
EXPECT_EQ(0u, num_generate_bid_calls);
// 3) The trusted bidding signals are loaded.
for (size_t i = 0; i < kNumGenerateBidCalls; ++i) {
AddBidderJsonResponse(
&url_loader_factory_,
GURL(base::StringPrintf(
"https://signals.test/"
"?hostname=top.window.test&keys=%zu&interestGroupNames=Fred",
i)),
base::StringPrintf(R"({"keys":{"%zu":%zu}})", i, i + 1), i);
}
// The worklets can now generate bids.
run_loop.Run();
EXPECT_EQ(kNumGenerateBidCalls, num_generate_bid_calls);
}
// It shouldn't matter the order in which network fetches complete. For each
// required and optional generateBid() URL load prerequisite, ensure that
// generateBid() completes when that URL is the last loaded URL.
TEST_F(BidderWorkletTest, GenerateBidLoadCompletionOrder) {
constexpr char kTrustedSignalsResponse[] = R"({"keys":{"1":1}})";
constexpr char kJsonResponse[] = "{}";
constexpr char kDirectFromSellerSignalsHeaders[] =
"X-Allow-FLEDGE: true\nX-FLEDGE-Auction-Only: true";
direct_from_seller_per_buyer_signals_ =
GURL("https://url.test/perbuyersignals");
direct_from_seller_auction_signals_ = GURL("https://url.test/auctionsignals");
interest_group_trusted_bidding_signals_url_ =
GURL("https://url.test/trustedsignals");
interest_group_trusted_bidding_signals_keys_ = {"1"};
struct Response {
GURL response_url;
std::string response_type;
std::string headers;
std::string content;
};
const Response kResponses[] = {
{interest_group_bidding_url_, kJavascriptMimeType, kAllowFledgeHeader,
CreateBasicGenerateBidScript()},
{*direct_from_seller_per_buyer_signals_, kJsonMimeType,
kDirectFromSellerSignalsHeaders, kJsonResponse},
{*direct_from_seller_auction_signals_, kJsonMimeType,
kDirectFromSellerSignalsHeaders, kJsonResponse},
{GURL(interest_group_trusted_bidding_signals_url_->spec() +
"?hostname=top.window.test&keys=1&interestGroupNames=Fred"),
kJsonMimeType, kAllowFledgeHeader, kTrustedSignalsResponse}};
// Cycle such that each response in `kResponses` gets to be the last response,
// like so:
//
// 0,1,2
// 1,2,0
// 2,0,1
for (size_t offset = 0; offset < std::size(kResponses); ++offset) {
SCOPED_TRACE(offset);
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
url_loader_factory_.ClearResponses();
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get());
for (size_t i = 0; i < std::size(kResponses); ++i) {
SCOPED_TRACE(i);
const Response& response =
kResponses[(i + offset) % std::size(kResponses)];
AddResponse(
&url_loader_factory_, response.response_url, response.response_type,
/*charset=*/absl::nullopt, response.content, response.headers);
task_environment_.RunUntilIdle();
if (i < std::size(kResponses) - 1) {
// Some URLs haven't finished loading -- generateBid() should be
// blocked.
EXPECT_FALSE(load_script_run_loop_->AnyQuitCalled());
}
}
// The last URL for this generateBid() call has completed -- check that
// generateBid() returns.
load_script_run_loop_->Run();
load_script_run_loop_.reset();
}
}
// If multiple worklets request DirectFromSellerSignals, they each get the
// correct signals.
TEST_F(BidderWorkletTest, GenerateBidDirectFromSellerSignalsMultipleWorklets) {
constexpr char kWorklet1JsonResponse[] = R"({"worklet":1})";
constexpr char kWorklet2JsonResponse[] = R"({"worklet":2})";
constexpr char kWorklet1ExtraCode[] = R"(
const perBuyerSignalsJson =
JSON.stringify(directFromSellerSignals.perBuyerSignals);
if (perBuyerSignalsJson !== '{"worklet":1}') {
throw 'Wrong directFromSellerSignals.perBuyerSignals ' +
perBuyerSignalsJson;
}
const auctionSignalsJson =
JSON.stringify(directFromSellerSignals.auctionSignals);
if (auctionSignalsJson !== '{"worklet":1}') {
throw 'Wrong directFromSellerSignals.auctionSignals ' +
auctionSignalsJson;
}
)";
constexpr char kWorklet2ExtraCode[] = R"(
const perBuyerSignalsJson =
JSON.stringify(directFromSellerSignals.perBuyerSignals);
if (perBuyerSignalsJson !== '{"worklet":2}') {
throw 'Wrong directFromSellerSignals.perBuyerSignals ' +
perBuyerSignalsJson;
}
const auctionSignalsJson =
JSON.stringify(directFromSellerSignals.auctionSignals);
if (auctionSignalsJson !== '{"worklet":2}') {
throw 'Wrong directFromSellerSignals.auctionSignals ' +
auctionSignalsJson;
}
)";
constexpr char kRawReturnValue[] =
R"({bid: 1, render:"https://response.test/"})";
constexpr char kDirectFromSellerSignalsHeaders[] =
"X-Allow-FLEDGE: true\nX-FLEDGE-Auction-Only: true";
for (bool late_direct_from_seller_signals : {false, true}) {
SCOPED_TRACE(late_direct_from_seller_signals);
provide_direct_from_seller_signals_late_ = late_direct_from_seller_signals;
direct_from_seller_per_buyer_signals_ =
GURL("https://url.test/perbuyersignals");
direct_from_seller_auction_signals_ =
GURL("https://url.test/auctionsignals");
mojo::Remote<mojom::BidderWorklet> bidder_worklet1 = CreateWorklet();
AddResponse(&url_loader_factory_, *direct_from_seller_per_buyer_signals_,
kJsonMimeType, /*charset=*/absl::nullopt, kWorklet1JsonResponse,
kDirectFromSellerSignalsHeaders);
AddResponse(&url_loader_factory_, *direct_from_seller_auction_signals_,
kJsonMimeType, /*charset=*/absl::nullopt, kWorklet1JsonResponse,
kDirectFromSellerSignalsHeaders);
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(/*raw_return_value=*/kRawReturnValue,
/*extra_code=*/kWorklet1ExtraCode));
// For the second worklet, use a different `interest_group_bidding_url_` (to
// set up different expectations), but use the same DirectFromSellerSignals
// URLs.
interest_group_bidding_url_ = GURL("https://url2.test/");
mojo::Remote<mojom::BidderWorklet> bidder_worklet2 =
CreateWorklet(/*url=*/GURL(),
/*pause_for_debugger_on_start=*/false,
/*out_bidder_worklet_impl=*/nullptr,
/*use_alternate_url_loader_factory=*/true);
AddResponse(&alternate_url_loader_factory_,
*direct_from_seller_per_buyer_signals_, kJsonMimeType,
/*charset=*/absl::nullopt, kWorklet2JsonResponse,
kDirectFromSellerSignalsHeaders);
AddResponse(&alternate_url_loader_factory_,
*direct_from_seller_auction_signals_, kJsonMimeType,
/*charset=*/absl::nullopt, kWorklet2JsonResponse,
kDirectFromSellerSignalsHeaders);
AddJavascriptResponse(
&alternate_url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(/*raw_return_value=*/kRawReturnValue,
/*extra_code=*/kWorklet2ExtraCode));
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet1.get());
load_script_run_loop_->Run();
EXPECT_THAT(bid_errors_, ::testing::UnorderedElementsAre());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet2.get());
load_script_run_loop_->Run();
EXPECT_THAT(bid_errors_, ::testing::UnorderedElementsAre());
load_script_run_loop_.reset();
}
}
TEST_F(BidderWorkletTest, GenerateBidAuctionSignals) {
const std::string kGenerateBidBody =
R"({ad: auctionSignals, bid:1, render:"https://response.test/"})";
// Since AuctionSignals are in JSON, non-JSON strings should result in
// failures.
auction_signals_ = "foo";
RunGenerateBidWithReturnValueExpectingResult(kGenerateBidBody,
mojom::BidderWorkletBidPtr());
auction_signals_ = R"("foo")";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("foo")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
auction_signals_ = "[1]";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"[1]", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidPerBuyerSignals) {
const std::string kGenerateBidBody =
R"({ad: perBuyerSignals, bid:1, render:"https://response.test/"})";
// Since PerBuyerSignals are in JSON, non-JSON strings should result in
// failures.
per_buyer_signals_ = "foo";
RunGenerateBidWithReturnValueExpectingResult(kGenerateBidBody,
mojom::BidderWorkletBidPtr());
per_buyer_signals_ = R"("foo")";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
R"("foo")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
per_buyer_signals_ = "[1]";
RunGenerateBidWithReturnValueExpectingResult(
kGenerateBidBody,
mojom::BidderWorkletBid::New(
"[1]", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
per_buyer_signals_ = absl::nullopt;
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: perBuyerSignals === null, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"true", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidBrowserSignalSellerOrigin) {
browser_signal_seller_origin_ =
url::Origin::Create(GURL("https://foo.test/"));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: browserSignals.seller, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"("https://foo.test")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
browser_signal_seller_origin_ =
url::Origin::Create(GURL("https://[::1]:40000/"));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: browserSignals.seller, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"("https://[::1]:40000")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidBrowserSignalTopLevelSellerOrigin) {
browser_signal_top_level_seller_origin_ = absl::nullopt;
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "topLevelSeller" in browserSignals,
bid:1,
render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"false", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Need to set `allowComponentAuction` to true for a bid to be created when
// topLevelSeller is non-null.
browser_signal_top_level_seller_origin_ =
url::Origin::Create(GURL("https://foo.test"));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: browserSignals.topLevelSeller, bid:1, render:"https://response.test/",
allowComponentAuction: true})",
mojom::BidderWorkletBid::New(
R"("https://foo.test")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidBrowserSignalTopWindowOrigin) {
top_window_origin_ = url::Origin::Create(GURL("https://top.window.test/"));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: browserSignals.topWindowHostname, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"("top.window.test")", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidBrowserSignalJoinCountBidCount) {
const struct IntegerTestCase {
// String used in JS to access the parameter.
const char* name;
// Pointer to location at which the integer can be modified.
raw_ptr<int> value_ptr;
} kIntegerTestCases[] = {
{"browserSignals.joinCount", &browser_signal_join_count_},
{"browserSignals.bidCount", &browser_signal_bid_count_}};
for (const auto& test_case : kIntegerTestCases) {
SCOPED_TRACE(test_case.name);
*test_case.value_ptr = 0;
RunGenerateBidWithReturnValueExpectingResult(
base::StringPrintf(
R"({ad: %s, bid:1, render:"https://response.test/"})",
test_case.name),
mojom::BidderWorkletBid::New(
"0", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
*test_case.value_ptr = 10;
RunGenerateBidWithReturnValueExpectingResult(
base::StringPrintf(
R"({ad: %s, bid:1, render:"https://response.test/"})",
test_case.name),
mojom::BidderWorkletBid::New(
"10", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
SetDefaultParameters();
}
}
TEST_F(BidderWorkletTest, GenerateBidAds) {
// A bid URL that's not in the InterestGroup's ads list should fail.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: 0, bid:1, render:"https://response2.test/"})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid render URL "
"'https://response2.test/' isn't one of the registered creative URLs."});
// Adding an ad with a corresponding `renderUrl` should result in success.
// Also check the `interestGroup.ads` field passed to Javascript.
interest_group_ads_.emplace_back(GURL("https://response2.test/"),
/*metadata=*/R"(["metadata"])");
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: interestGroup.ads, bid:1, render:"https://response2.test/"})",
mojom::BidderWorkletBid::New(
"[{\"renderUrl\":\"https://response.test/\"},"
"{\"renderUrl\":\"https://response2.test/"
"\",\"metadata\":[\"metadata\"]}]",
1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response2.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Make sure `metadata` is treated as an object, instead of a raw string.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: interestGroup.ads[1].metadata[0], bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"\"metadata\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidAdComponents) {
// Basic test with an adComponent URL.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: 0, bid:1, render:"https://response.test/", adComponents:["https://ad_component.test/"]})",
mojom::BidderWorkletBid::New(
"0", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
std::vector<blink::AdDescriptor>{
blink::AdDescriptor(GURL("https://ad_component.test/"))},
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Empty, but non-null, adComponents field.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: 0, bid:1, render:"https://response.test/", adComponents:[]})",
mojom::BidderWorkletBid::New(
"0", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
std::vector<blink::AdDescriptor>(),
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// An adComponent URL that's not in the InterestGroup's adComponents list
// should fail.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: 0, bid:1, render:"https://response.test/", adComponents:["https://response.test/"]})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() bid adComponents URL "
"'https://response.test/' isn't one of the registered creative URLs."});
// Add a second ad component URL, this time with metadata.
// Returning a list with both ads should result in success.
// Also check the `interestGroup.ads` field passed to Javascript.
interest_group_ad_components_->emplace_back(
GURL("https://ad_component2.test/"), /*metadata=*/R"(["metadata"])");
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: interestGroup.adComponents,
bid:1,
render:"https://response.test/",
adComponents:["https://ad_component.test/", "https://ad_component2.test/"]})",
mojom::BidderWorkletBid::New(
"[{\"renderUrl\":\"https://ad_component.test/\"},"
"{\"renderUrl\":\"https://ad_component2.test/"
"\",\"metadata\":[\"metadata\"]}]",
1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
std::vector<blink::AdDescriptor>{
blink::AdDescriptor(GURL("https://ad_component.test/")),
blink::AdDescriptor(GURL("https://ad_component2.test/"))},
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
// Test behavior of the `allowComponentAuction` output field, which can block
// bids when not set to true and `topLevelSellerOrigin` is non-null.
TEST_F(BidderWorkletTest, GenerateBidAllowComponentAuction) {
// In all success cases, this is the returned bid.
const auto kBidOnSuccess = mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta());
// Use a null `topLevelSellerOrigin`. `allowComponentAuction` value should be
// ignored.
browser_signal_top_level_seller_origin_ = absl::nullopt;
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: true})",
kBidOnSuccess.Clone());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: false})",
kBidOnSuccess.Clone());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/"})",
kBidOnSuccess.Clone());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: 0})",
kBidOnSuccess.Clone());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: 1})",
kBidOnSuccess.Clone());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: "OnTuesdays"})",
kBidOnSuccess.Clone());
// Use a non-null `topLevelSellerOrigin`. `allowComponentAuction` value must
// be "true" for a bid to be generated. This uses the standard Javascript
// behavior for how to convert non-bools to a bool.
browser_signal_top_level_seller_origin_ =
url::Origin::Create(GURL("https://foo.test"));
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: true})",
kBidOnSuccess.Clone());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: false})",
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid does not have "
"allowComponentAuction set to true. Bid dropped from component "
"auction."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid does not have "
"allowComponentAuction set to true. Bid dropped from component "
"auction."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: 0})",
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid does not have "
"allowComponentAuction set to true. Bid dropped from component "
"auction."});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: 1})",
kBidOnSuccess.Clone());
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: null, bid:1, render:"https://response.test/", allowComponentAuction: "OnTuesdays"})",
kBidOnSuccess.Clone());
}
TEST_F(BidderWorkletTest, GenerateBidWasm404) {
interest_group_wasm_url_ = GURL(kWasmUrl);
// Have the WASM URL 404.
AddResponse(&url_loader_factory_, interest_group_wasm_url_.value(),
kWasmMimeType,
/*charset=*/absl::nullopt, "Error 404", kAllowFledgeHeader,
net::HTTP_NOT_FOUND);
// The Javascript request receives valid JS.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateBasicGenerateBidScript());
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingNeverCompletes(bidder_worklet.get());
EXPECT_EQ(
"Failed to load https://foo.test/helper.wasm "
"HTTP status = 404 Not Found.",
WaitForDisconnect());
}
TEST_F(BidderWorkletTest, GenerateBidWasmFailure) {
interest_group_wasm_url_ = GURL(kWasmUrl);
// Instead of WASM have JS, but with WASM mimetype.
AddResponse(&url_loader_factory_, interest_group_wasm_url_.value(),
kWasmMimeType,
/*charset=*/absl::nullopt, CreateBasicGenerateBidScript());
// The Javascript request receives valid JS.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateBasicGenerateBidScript());
auto bidder_worklet = CreateWorklet();
GenerateBidExpectingNeverCompletes(bidder_worklet.get());
EXPECT_EQ(
"https://foo.test/helper.wasm Uncaught CompileError: "
"WasmModuleObject::Compile(): expected magic word 00 61 73 6d, found "
"0a 20 20 20 @+0.",
WaitForDisconnect());
}
TEST_F(BidderWorkletTest, GenerateBidWasm) {
std::string bid_script = CreateGenerateBidScript(
R"({ad: WebAssembly.Module.exports(browserSignals.wasmHelper), bid: 1,
render:"https://response.test/"})");
interest_group_wasm_url_ = GURL(kWasmUrl);
AddResponse(&url_loader_factory_, GURL(kWasmUrl), kWasmMimeType,
/*charset=*/absl::nullopt, ToyWasm());
RunGenerateBidWithJavascriptExpectingResult(
bid_script, mojom::BidderWorkletBid::New(
R"([{"name":"test_const","kind":"global"}])", 1,
/*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, WasmReportWin) {
// Regression test for state machine bug during development, with
// double-execution of report win tasks when waiting on WASM.
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateReportWinScript(R"(sendReportTo("https://foo.test"))"));
interest_group_wasm_url_ = GURL(kWasmUrl);
auto bidder_worklet = CreateWorklet();
ASSERT_TRUE(bidder_worklet);
// Wedge the V8 thread so that completed first instance of reportWin doesn't
// fully wrap up and clean up the task by time the WASM is delivered.
base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get());
base::RunLoop run_loop;
bidder_worklet->ReportWin(
reporting_id_field_, reporting_id_, /*auction_signals_json=*/"0",
per_buyer_signals_, direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, seller_signals_,
browser_signal_render_url_, browser_signal_bid_,
browser_signal_bid_currency_, browser_signal_highest_scoring_other_bid_,
browser_signal_highest_scoring_other_bid_currency_,
browser_signal_made_highest_scoring_other_bid_, browser_signal_ad_cost_,
browser_signal_modeling_signals_, browser_signal_join_count_,
browser_signal_recency_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, data_version_.value_or(0),
data_version_.has_value(),
/*trace_id=*/1,
base::BindLambdaForTesting(
[&run_loop](const absl::optional<GURL>& report_url,
const base::flat_map<std::string, GURL>& ad_beacon_map,
PrivateAggregationRequests pa_requests,
const std::vector<std::string>& errors) {
run_loop.Quit();
}));
base::RunLoop().RunUntilIdle();
AddResponse(&url_loader_factory_, GURL(kWasmUrl), kWasmMimeType,
/*charset=*/absl::nullopt, ToyWasm());
base::RunLoop().RunUntilIdle();
event_handle->Signal();
run_loop.Run();
// Make sure there isn't a second attempt to complete lurking.
task_environment_.RunUntilIdle();
}
TEST_F(BidderWorkletTest, WasmReportWin2) {
// Regression test for https://crbug.com/1345219 --- JS loaded but WASM not
// by time of reportWin() causing it to not get run.
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateReportWinScript(R"(sendReportTo("https://foo.test"))"));
interest_group_wasm_url_ = GURL(kWasmUrl);
auto bidder_worklet = CreateWorklet();
ASSERT_TRUE(bidder_worklet);
// Get the JS a chance to load.
task_environment_.RunUntilIdle();
base::RunLoop run_loop;
RunReportWinExpectingResultAsync(
bidder_worklet.get(), GURL("https://foo.test"),
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
/*expected_errors=*/{},
base::BindLambdaForTesting([&run_loop]() { run_loop.Quit(); }));
task_environment_.RunUntilIdle();
AddResponse(&url_loader_factory_, GURL(kWasmUrl), kWasmMimeType,
/*charset=*/absl::nullopt, ToyWasm());
task_environment_.RunUntilIdle();
run_loop.Run();
}
TEST_F(BidderWorkletTest, WasmOrdering) {
enum Event { kWasmSuccess, kJsSuccess, kWasmFailure, kJsFailure };
struct Test {
std::vector<Event> events;
bool expect_success;
};
const Test tests[] = {
{{kWasmSuccess, kJsSuccess}, /*expect_success=*/true},
{{kJsSuccess, kWasmSuccess}, /*expect_success=*/true},
{{kWasmFailure, kJsSuccess}, /*expect_success=*/false},
{{kJsSuccess, kWasmFailure}, /*expect_success=*/false},
{{kWasmSuccess, kJsFailure}, /*expect_success=*/false},
{{kJsFailure, kWasmSuccess}, /*expect_success=*/false},
{{kJsFailure, kWasmFailure}, /*expect_success=*/false},
{{kWasmFailure, kJsFailure}, /*expect_success=*/false},
};
interest_group_wasm_url_ = GURL(kWasmUrl);
for (const Test& test : tests) {
url_loader_factory_.ClearResponses();
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
if (test.expect_success) {
// On success, callback should be invoked.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get());
} else {
// On error, the pipe is closed without invoking the callback.
GenerateBidExpectingNeverCompletes(bidder_worklet.get());
}
for (Event ev : test.events) {
switch (ev) {
case kWasmSuccess:
AddResponse(&url_loader_factory_, GURL(kWasmUrl),
std::string(kWasmMimeType),
/*charset=*/absl::nullopt, ToyWasm());
break;
case kJsSuccess:
AddJavascriptResponse(&url_loader_factory_,
interest_group_bidding_url_,
CreateBasicGenerateBidScript());
break;
case kWasmFailure:
url_loader_factory_.AddResponse(kWasmUrl, "", net::HTTP_NOT_FOUND);
break;
case kJsFailure:
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
"", net::HTTP_NOT_FOUND);
break;
}
task_environment_.RunUntilIdle();
}
if (test.expect_success) {
// On success, the callback is invoked.
load_script_run_loop_->Run();
load_script_run_loop_.reset();
EXPECT_TRUE(bid_);
} else {
// On failure, the pipe is closed with a non-empty error message, without
// invoking the callback.
EXPECT_FALSE(WaitForDisconnect().empty());
}
}
}
// Utility method to create a vector of PreviousWin. Needed because StructPtrs
// don't allow copying.
std::vector<mojom::PreviousWinPtr> CreateWinList(
const mojom::PreviousWinPtr& win1,
const mojom::PreviousWinPtr& win2 = mojom::PreviousWinPtr(),
const mojom::PreviousWinPtr& win3 = mojom::PreviousWinPtr()) {
std::vector<mojo::StructPtr<mojom::PreviousWin>> out;
out.emplace_back(win1.Clone());
if (win2) {
out.emplace_back(win2.Clone());
}
if (win3) {
out.emplace_back(win3.Clone());
}
return out;
}
TEST_F(BidderWorkletTest, GenerateBidPrevWins) {
base::TimeDelta delta = base::Seconds(100);
base::TimeDelta tiny_delta = base::Milliseconds(500);
base::Time time1 = auction_start_time_ - delta - delta;
base::Time time2 = auction_start_time_ - delta - tiny_delta;
base::Time future_time = auction_start_time_ + delta;
auto win1 = mojom::PreviousWin::New(time1, R"("ad1")");
auto win2 = mojom::PreviousWin::New(time2, R"(["ad2"])");
auto future_win = mojom::PreviousWin::New(future_time, R"("future_ad")");
struct TestCase {
std::vector<mojo::StructPtr<mojom::PreviousWin>> prev_wins;
// Value to output as the ad data.
const char* ad;
// Expected output in the `ad` field of the result.
const char* expected_ad;
} test_cases[] = {
{
{},
"browserSignals.prevWins",
"[]",
},
{
CreateWinList(win1),
"browserSignals.prevWins",
R"([[200,"ad1"]])",
},
// Make sure it's passed on as an object and not a string.
{
CreateWinList(win1),
"browserSignals.prevWins[0]",
R"([200,"ad1"])",
},
// Test rounding.
{
CreateWinList(win2),
"browserSignals.prevWins",
R"([[100,["ad2"]]])",
},
// Multiple previous wins.
{
CreateWinList(win1, win2),
"browserSignals.prevWins",
R"([[200,"ad1"],[100,["ad2"]]])",
},
// Times are trimmed at 0.
{
CreateWinList(future_win),
"browserSignals.prevWins",
R"([[0,"future_ad"]])",
},
// Out of order wins should be sorted.
{
CreateWinList(win2, future_win, win1),
"browserSignals.prevWins",
R"([[200,"ad1"],[100,["ad2"]],[0,"future_ad"]])",
},
};
for (auto& test_case : test_cases) {
SCOPED_TRACE(test_case.ad);
// StructPtrs aren't copiable, so this effectively destroys each test case.
browser_signal_prev_wins_ = std::move(test_case.prev_wins);
RunGenerateBidWithReturnValueExpectingResult(
base::StringPrintf(
R"({ad: %s, bid:1, render:"https://response.test/"})",
test_case.ad),
mojom::BidderWorkletBid::New(
test_case.expected_ad, 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
}
TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignals) {
const GURL kBaseSignalsUrl("https://signals.test/");
const GURL kNoKeysSignalsUrl(
"https://signals.test/"
"?hostname=top.window.test&interestGroupNames=Fred");
const GURL kFullSignalsUrl(
"https://signals.test/"
"?hostname=top.window.test&keys=key1,key2&interestGroupNames=Fred");
const char kJson[] = R"(
{
"keys": {
"key1": 1,
"key2": [2]
}
}
)";
size_t observed_requests = 0;
// Request with null TrustedBiddingSignals keys and URL. No request should be
// made.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(observed_requests += 1, url_loader_factory_.total_requests());
// Request with TrustedBiddingSignals keys and null URL. No request should be
// made.
interest_group_trusted_bidding_signals_keys_ =
std::vector<std::string>({"key1", "key2"});
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(observed_requests += 1, url_loader_factory_.total_requests());
// Request with TrustedBiddingSignals URL and null keys. Request should be
// made without any keys, and nothing from the response passed to
// generateBid().
interest_group_trusted_bidding_signals_url_ = kBaseSignalsUrl;
interest_group_trusted_bidding_signals_keys_.reset();
AddBidderJsonResponse(&url_loader_factory_, kNoKeysSignalsUrl, kJson);
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(observed_requests += 2, url_loader_factory_.total_requests());
// Request with TrustedBiddingSignals URL and empty keys. Request should be
// made without any keys, and nothing from the response passed to
// generateBid().
interest_group_trusted_bidding_signals_keys_ = std::vector<std::string>();
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(observed_requests += 2, url_loader_factory_.total_requests());
url_loader_factory_.ClearResponses();
// Request with valid TrustedBiddingSignals URL and non-empty keys. Request
// should be made. The request fails.
interest_group_trusted_bidding_signals_keys_ =
std::vector<std::string>({"key1", "key2"});
url_loader_factory_.AddResponse(kFullSignalsUrl.spec(), kJson,
net::HTTP_NOT_FOUND);
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
{"Failed to load "
"https://signals.test/"
"?hostname=top.window.test&keys=key1,key2&interestGroupNames=Fred HTTP "
"status = 404 Not Found."});
EXPECT_EQ(observed_requests += 2, url_loader_factory_.total_requests());
// Request with valid TrustedBiddingSignals URL and non-empty keys. Request
// should be made. The request succeeds.
AddBidderJsonResponse(&url_loader_factory_, kFullSignalsUrl, kJson);
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"({"key1":1,"key2":[2]})", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
EXPECT_EQ(observed_requests += 2, url_loader_factory_.total_requests());
}
TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignalsV1) {
const GURL kFullSignalsUrl(
"https://signals.test/"
"?hostname=top.window.test&keys=key1,key2&interestGroupNames=Fred");
const char kJson[] = R"(
{
"key1": 1,
"key2": [2]
}
)";
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
interest_group_trusted_bidding_signals_keys_ =
std::vector<std::string>({"key1", "key2"});
// Request with valid TrustedBiddingSignals URL and non-empty keys. Request
// should be made. The request succeeds.
AddBidderJsonResponse(&url_loader_factory_, kFullSignalsUrl, kJson,
/*data_version=*/absl::nullopt,
/*format_version_string=*/absl::nullopt);
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"({"key1":1,"key2":[2]})", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"Bidding signals URL https://signals.test/ is using outdated bidding "
"signals format. Consumers should be updated to use bidding signals "
"format version 2"});
}
// Test that when no trusted signals are fetched, generating bids is delayed
// until the OnBiddingSignalsReceivedCallback is invoked.
TEST_F(BidderWorkletTest, GenerateBidOnBiddingSignalsReceivedNoTrustedSignals) {
auto bidder_worklet = CreateWorklet();
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(CreateBasicGenerateBidScript()));
base::RunLoop on_bidding_signals_received_run_loop;
base::OnceClosure on_bidding_signals_received_continue_callback;
auto on_bidding_signals_received_callback = base::BindLambdaForTesting(
[&](const base::flat_map<std::string, double>& priority_vector,
base::TimeDelta trusted_signals_fetch_latency,
base::OnceClosure callback) {
EXPECT_TRUE(priority_vector.empty());
EXPECT_EQ(base::TimeDelta(), trusted_signals_fetch_latency);
on_bidding_signals_received_run_loop.Quit();
on_bidding_signals_received_continue_callback = std::move(callback);
});
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get(),
GenerateBidClientWithCallbacks::Create(
base::BindOnce(&BidderWorkletTest::GenerateBidCallback,
base::Unretained(this)),
std::move(on_bidding_signals_received_callback)));
task_environment_.RunUntilIdle();
EXPECT_TRUE(on_bidding_signals_received_run_loop.AnyQuitCalled());
EXPECT_FALSE(load_script_run_loop_->AnyQuitCalled());
ASSERT_TRUE(on_bidding_signals_received_continue_callback);
std::move(on_bidding_signals_received_continue_callback).Run();
load_script_run_loop_->Run();
}
// Test that when signals fail to be feteched, OnBiddingSignalsReceived() is
// only invoked after the failed fetch completes, and generating bids is delayed
// until the OnBiddingSignalsReceivedCallback is invoked.
TEST_F(BidderWorkletTest, GenerateBidOnBiddingSignalsReceivedFetchFails) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
const GURL kFullSignalsUrl(
"https://signals.test/?hostname=top.window.test&interestGroupNames=Fred");
auto bidder_worklet = CreateWorklet();
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(CreateBasicGenerateBidScript()));
base::RunLoop on_bidding_signals_received_run_loop;
base::OnceClosure on_bidding_signals_received_continue_callback;
auto on_bidding_signals_received_callback = base::BindLambdaForTesting(
[&](const base::flat_map<std::string, double>& priority_vector,
base::TimeDelta trusted_signals_fetch_latency,
base::OnceClosure callback) {
EXPECT_TRUE(priority_vector.empty());
on_bidding_signals_received_run_loop.Quit();
on_bidding_signals_received_continue_callback = std::move(callback);
});
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get(),
GenerateBidClientWithCallbacks::Create(
base::BindOnce(&BidderWorkletTest::GenerateBidCallback,
base::Unretained(this)),
std::move(on_bidding_signals_received_callback)));
task_environment_.RunUntilIdle();
EXPECT_FALSE(on_bidding_signals_received_run_loop.AnyQuitCalled());
EXPECT_FALSE(load_script_run_loop_->AnyQuitCalled());
url_loader_factory_.AddResponse(kFullSignalsUrl.spec(), /*content=*/"",
net::HTTP_NOT_FOUND);
task_environment_.RunUntilIdle();
EXPECT_TRUE(on_bidding_signals_received_run_loop.AnyQuitCalled());
EXPECT_FALSE(load_script_run_loop_->AnyQuitCalled());
ASSERT_TRUE(on_bidding_signals_received_continue_callback);
std::move(on_bidding_signals_received_continue_callback).Run();
load_script_run_loop_->Run();
}
// Test that when signals are successfully fetched, OnBiddingSignalsReceived()
// is only invoked after the fetch completes, and generating bids is delayed
// until the OnBiddingSignalsReceivedCallback is invoked.
TEST_F(BidderWorkletTest,
GenerateBidOnBiddingSignalsReceivedNoPriorityVectorRequested) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
interest_group_trusted_bidding_signals_keys_ = {{"key1"}};
const GURL kFullSignalsUrl(
"https://signals.test/"
"?hostname=top.window.test&keys=key1&interestGroupNames=Fred");
const char kJson[] = R"({"keys": {"key1": 1}})";
auto bidder_worklet = CreateWorklet();
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(CreateBasicGenerateBidScript()));
base::RunLoop on_bidding_signals_received_run_loop;
base::OnceClosure on_bidding_signals_received_continue_callback;
auto on_bidding_signals_received_callback = base::BindLambdaForTesting(
[&](const base::flat_map<std::string, double>& priority_vector,
base::TimeDelta trusted_signals_fetch_latency,
base::OnceClosure callback) {
EXPECT_TRUE(priority_vector.empty());
on_bidding_signals_received_run_loop.Quit();
on_bidding_signals_received_continue_callback = std::move(callback);
});
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get(),
GenerateBidClientWithCallbacks::Create(
base::BindOnce(&BidderWorkletTest::GenerateBidCallback,
base::Unretained(this)),
std::move(on_bidding_signals_received_callback)));
task_environment_.RunUntilIdle();
EXPECT_FALSE(on_bidding_signals_received_run_loop.AnyQuitCalled());
EXPECT_FALSE(load_script_run_loop_->AnyQuitCalled());
AddBidderJsonResponse(&url_loader_factory_, kFullSignalsUrl, kJson);
task_environment_.RunUntilIdle();
EXPECT_TRUE(on_bidding_signals_received_run_loop.AnyQuitCalled());
EXPECT_FALSE(load_script_run_loop_->AnyQuitCalled());
ASSERT_TRUE(on_bidding_signals_received_continue_callback);
std::move(on_bidding_signals_received_continue_callback).Run();
load_script_run_loop_->Run();
}
// Test that when signals are successfully fetched, but the response includes no
// priority vector. OnBiddingSignalsReceived() is invoked with a empty priority
// vector after the fetch completes, and generating bids is delayed until the
// OnBiddingSignalsReceivedCallback is invoked.
TEST_F(BidderWorkletTest,
GenerateBidOnBiddingSignalsReceivedNoPriorityVectorReceived) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
const GURL kFullSignalsUrl(
"https://signals.test/?hostname=top.window.test&interestGroupNames=Fred");
const char kJson[] = "{}";
auto bidder_worklet = CreateWorklet();
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(CreateBasicGenerateBidScript()));
base::RunLoop on_bidding_signals_received_run_loop;
base::OnceClosure on_bidding_signals_received_continue_callback;
auto on_bidding_signals_received_callback = base::BindLambdaForTesting(
[&](const base::flat_map<std::string, double>& priority_vector,
base::TimeDelta trusted_signals_fetch_latency,
base::OnceClosure callback) {
EXPECT_TRUE(priority_vector.empty());
on_bidding_signals_received_run_loop.Quit();
on_bidding_signals_received_continue_callback = std::move(callback);
});
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get(),
GenerateBidClientWithCallbacks::Create(
base::BindOnce(&BidderWorkletTest::GenerateBidCallback,
base::Unretained(this)),
std::move(on_bidding_signals_received_callback)));
task_environment_.RunUntilIdle();
EXPECT_FALSE(on_bidding_signals_received_run_loop.AnyQuitCalled());
EXPECT_FALSE(load_script_run_loop_->AnyQuitCalled());
AddBidderJsonResponse(&url_loader_factory_, kFullSignalsUrl, kJson);
task_environment_.RunUntilIdle();
EXPECT_TRUE(on_bidding_signals_received_run_loop.AnyQuitCalled());
EXPECT_FALSE(load_script_run_loop_->AnyQuitCalled());
ASSERT_TRUE(on_bidding_signals_received_continue_callback);
std::move(on_bidding_signals_received_continue_callback).Run();
load_script_run_loop_->Run();
}
// Test that cancelling a GenerateBid() call by deleting the GenerateBidClient
// aborts a pending trusted signals fetch.
TEST_F(BidderWorkletTest, GenerateBidCancelAbortsSignalsFetch) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
const GURL kFullSignalsUrl(
"https://signals.test/?hostname=top.window.test&interestGroupNames=Fred");
auto bidder_worklet = CreateWorklet();
// This is not strictly needed for this test, but should ensure that only the
// JSON fetch is pending.
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(CreateBasicGenerateBidScript()));
GenerateBidClientWithCallbacks client(
GenerateBidClientWithCallbacks::GenerateBidNeverInvokedCallback(),
GenerateBidClientWithCallbacks::OnBiddingSignalsReceivedCallback());
mojo::AssociatedReceiver<mojom::GenerateBidClient> client_receiver(&client);
GenerateBid(bidder_worklet.get(),
client_receiver.BindNewEndpointAndPassRemote());
// Wait for the JSON request to be made, and validate it's URL and that the
// URLLoaderClient pipe is still connected.
task_environment_.RunUntilIdle();
ASSERT_EQ(1, url_loader_factory_.NumPending());
EXPECT_EQ(kFullSignalsUrl,
url_loader_factory_.GetPendingRequest(0)->request.url);
ASSERT_TRUE(url_loader_factory_.GetPendingRequest(0)->client.is_connected());
// Destroy the GenerateBidClient pipe. This should result in the generate bid
// call being aborted, and the fetch being cancelled, since only the cancelled
// GenerateBid() call was depending on the response.
client_receiver.reset();
task_environment_.RunUntilIdle();
ASSERT_FALSE(url_loader_factory_.GetPendingRequest(0)->client.is_connected());
}
// Test that cancelling a GenerateBid() call by deleting the GenerateBidClient
// when Javascript is running doesn't result in a crash.
TEST_F(BidderWorkletTest, GenerateBidCancelWhileRunningJavascript) {
auto bidder_worklet = CreateWorklet();
// Use a hanging GenerateBid() script so this it (most likely) completes only
// after the close pipe message has been received.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript("while (true);"));
// Reduce the script timeout so the test doesn't take too long to run, while
// waiting for the loop in the above script to timeout.
per_buyer_timeout_ = base::Milliseconds(10);
base::OnceClosure on_bidding_signals_received_continue_callback;
auto on_bidding_signals_received_callback = base::BindLambdaForTesting(
[&](const base::flat_map<std::string, double>& priority_vector,
base::TimeDelta trusted_signals_fetch_latency,
base::OnceClosure callback) {
EXPECT_TRUE(priority_vector.empty());
on_bidding_signals_received_continue_callback = std::move(callback);
});
GenerateBidClientWithCallbacks client(
GenerateBidClientWithCallbacks::GenerateBidNeverInvokedCallback(),
std::move(on_bidding_signals_received_callback));
mojo::AssociatedReceiver<mojom::GenerateBidClient> client_receiver(&client);
load_script_run_loop_ = std::make_unique<base::RunLoop>();
GenerateBid(bidder_worklet.get(),
client_receiver.BindNewEndpointAndPassRemote());
// Wait for for the Javascript to be fetched and OnSignalsReceived to be
// invoked.
task_environment_.RunUntilIdle();
ASSERT_TRUE(on_bidding_signals_received_continue_callback);
// Invoking the callback passed to OnBiddingSignalsReceived() will cause a
// task to be posted to run the Javascript generateBid() method.
std::move(on_bidding_signals_received_continue_callback).Run();
// This deletes the BidderWorklet's GenerateBidTask for the already running
// Javascript. Once the Javascript completes running and posts a task back to
// the main thread, that task should not dereference the deleted
// GenerateBidTask, because it uses a weak pointer.
client_receiver.reset();
// Wait until the script is finished. There should be no crash.
task_environment_.RunUntilIdle();
}
TEST_F(BidderWorkletTest, GenerateBidDataVersion) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
interest_group_trusted_bidding_signals_keys_.emplace();
interest_group_trusted_bidding_signals_keys_->push_back("key1");
AddBidderJsonResponse(
&url_loader_factory_,
GURL("https://signals.test/"
"?hostname=top.window.test&keys=key1&interestGroupNames=Fred"),
R"({"keys":{"key1":1}})", /*data_version=*/7u);
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:browserSignals.dataVersion, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"("ad")", 7, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/7u);
}
// Even with no trustedBiddingSignalsKeys, the data version should be available.
TEST_F(BidderWorkletTest, GenerateBidDataVersionNoKeys) {
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
AddBidderJsonResponse(
&url_loader_factory_,
GURL("https://signals.test/"
"?hostname=top.window.test&interestGroupNames=Fred"),
R"({})", /*data_version=*/7u);
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:browserSignals.dataVersion, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"("ad")", 7, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/7u);
}
// Even though the script had set an intermediate result with setBid, the
// returned value should be used instead.
TEST_F(BidderWorkletTest, GenerateBidWithSetBid) {
interest_group_ads_.emplace_back(GURL("https://response.test/replacement"),
/*metadata=*/absl::nullopt);
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/
R"({ad: "returned", bid:2, render:"https://response.test/replacement" })",
/*extra_code=*/R"(
setBid({ad: "ad", bid:1, render:"https://response.test/"})
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"returned\"", 2, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/replacement")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// Test the no-bid version as well.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/
R"({ad: "returned", bid:0, render:"https://response.test/" })",
/*extra_code=*/R"(
setBid({ad: "ad", bid:1, render:"https://response.test/"})
)"),
/*expected_bid=*/
mojom::BidderWorkletBidPtr());
}
TEST_F(BidderWorkletTest, GenerateBidExperimentGroupId) {
experiment_group_id_ = 48384u;
interest_group_trusted_bidding_signals_url_ = GURL("https://signals.test/");
interest_group_trusted_bidding_signals_keys_.emplace();
interest_group_trusted_bidding_signals_keys_->push_back("key1");
AddBidderJsonResponse(
&url_loader_factory_,
GURL("https://signals.test/"
"?hostname=top.window.test&keys=key1&interestGroupNames=Fred"
"&experimentGroupId=48384"),
R"({"keys":{"key1":1}})");
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:123, render:"https://response.test/"})",
mojom::BidderWorkletBid::New(
R"("ad")", 123, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, GenerateBidTimedOut) {
// The bidding script has an endless while loop. It will time out due to
// AuctionV8Helper's default script timeout (50 ms).
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(/*raw_return_value=*/"", R"(while (1))"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."});
}
TEST_F(BidderWorkletTest, GenerateBidPerBuyerTimeOut) {
// Use a very long default script timeout, and a short per buyer timeout, so
// that if the bidder script with endless loop times out, we know that the per
// buyer timeout overwrote the default script timeout and worked.
const base::TimeDelta kScriptTimeout = base::Days(360);
v8_helper_->v8_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](scoped_refptr<AuctionV8Helper> v8_helper,
const base::TimeDelta script_timeout) {
v8_helper->set_script_timeout_for_testing(script_timeout);
},
v8_helper_, kScriptTimeout));
// Make sure set_script_timeout_for_testing is called.
task_environment_.RunUntilIdle();
per_buyer_timeout_ = base::Milliseconds(20);
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(/*raw_return_value=*/"", R"(while (1))"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."});
}
// Even though the script timed out, it had set an intermediate result with
// setBid, so we should use that. Note that this test sets `bid_duration` to
// `AuctionV8Helper::kScriptTimeout`, to make sure the full timeout time is
// included in the duration.
TEST_F(BidderWorkletTest, GenerateBidTimedOutWithSetBid) {
// The bidding script has an endless while loop.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/
R"({ad: "not_reached", bid:2, render:"https://response.test/2" })",
/*extra_code=*/R"(
setBid({ad: "ad", bid:1, render:"https://response.test/"});
while (1)
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, AuctionV8Helper::kScriptTimeout),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."});
// Test the case where setBid() sets no bid. There should be no bid, and
// no error, other than the timeout.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/
R"({ad: "not_reached", bid:2, render:"https://response.test/2" })",
/*extra_code=*/R"(
setBid();
while (1)
)"),
/*expected_bid=*/
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."});
}
// Test that per-buyer timeout of zero results in no bid produced.
TEST_F(BidderWorkletTest, TimeoutZero) {
per_buyer_timeout_ = base::Seconds(0);
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid:1, render:"https://response.test/"})",
mojom::BidderWorkletBidPtr());
}
// Test that in the case of multiple setBid() calls, the most recent call takes
// precedence. Note that this test sets `bid_duration` to
// `AuctionV8Helper::kScriptTimeout`, to make sure the full timeout time is
// included in the duration.
TEST_F(BidderWorkletTest, GenerateBidTimedOutWithSetBidTwice) {
interest_group_ads_.emplace_back(GURL("https://response.test/replacement"),
/*metadata=*/absl::nullopt);
// The bidding script has an endless while loop.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/
R"({ad: "not_reached", bid:2, render:"https://response.test/2" })",
/*extra_code=*/R"(
setBid({ad: "ad", bid:1, render:"https://response.test/"});
setBid({ad: "ad2", bid:2, render:"https://response.test/replacement"});
while (1)
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad2\"", 2, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/replacement")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, AuctionV8Helper::kScriptTimeout),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."});
// Test the case where the second setBid() call clears the bid from the first
// call without setting a new bid, by passing in no bid. There should be no
// bid, and no error, other than the timeout.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/
R"({ad: "not_reached", bid:2, render:"https://response.test/2" })",
/*extra_code=*/R"(
setBid({ad: "ad", bid:1, render:"https://response.test/"});
setBid(null);
while (1)
)"),
/*expected_bid=*/
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."});
// Test the case where the second setBid() call clears the bid from the first
// call without setting a new bid, by passing in null. There should be no bid,
// and no error, other than the timeout.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/
R"({ad: "not_reached", bid:2, render:"https://response.test/2" })",
/*extra_code=*/R"(
setBid({ad: "ad", bid:1, render:"https://response.test/"});
setBid(null);
while (1)
)"),
/*expected_bid=*/
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."});
}
// Even though the script timed out, it had set an intermediate result with
// setBid, so we should use that instead. The bid value should not change if we
// mutate the object passed to setBid after it returns.
TEST_F(BidderWorkletTest, GenerateBidTimedOutWithSetBidMutateAfter) {
// The bidding script has an endless while loop.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/
R"({ad: "not_reached", bid:2, render:"https://response.test/2" })",
/*extra_code=*/R"(
let result = {ad: "ad", bid:1, render:"https://response.test/"};
setBid(result);
result.ad = "ad2";
result.bid = 3;
result.render = "https://response.test/3";
while (1)
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."});
}
TEST_F(BidderWorkletTest, GenerateBidSetPriority) {
// not enough args
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPriority();
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught TypeError: setPriority requires 1 double "
"parameter."});
// priority not a double
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPriority("string");
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught TypeError: setPriority requires 1 double "
"parameter."});
// priority not finite
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPriority(0.0/0.0);
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught TypeError: setPriority requires 1 finite "
"double parameter."});
// priority called twice
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPriority(4);
setPriority(4);
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:7 Uncaught TypeError: setPriority may be called at "
"most once."});
// success
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPriority(9.0);
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/9.0);
// set priority with no bid should work.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:0, render:"https://response.test/" })",
/*extra_code=*/R"(
setPriority(9.0);
)"),
/*expected_bid=*/
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/9.0);
// set priority with an invalid bid should work.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1/0, render:"https://response.test/" })",
/*extra_code=*/R"(
setPriority(9.0);
)"),
/*expected_bid=*/
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/ generateBid() bid of inf is not a valid bid."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/9.0);
}
TEST_F(BidderWorkletTest, GenerateBidSetPrioritySignalsOverrides) {
// Not enough args.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride();
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught TypeError: setPrioritySignalsOverride "
"requires at least 1 parameter."});
// Key not a string.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride(15, 15);
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught TypeError: First argument to "
"setPrioritySignalsOverride must be a String."});
// Value not a double.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key", "value");
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught TypeError: Second argument to "
"setPrioritySignalsOverride must be a finite Number or null."});
// Value not finite.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key", 0.0/0.0);
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught TypeError: Second argument to "
"setPrioritySignalsOverride must be a finite Number or null."});
// A key with no value means the value should be removed.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key");
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{{"key", absl::nullopt}});
// An undefined value means the value should be removed.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key", undefined);
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{{"key", absl::nullopt}});
// A null value means the value should be removed.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key", null);
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{{"key", absl::nullopt}});
// Set a number.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key", 0);
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{{"key", 0}});
// Set a value multiple times.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key", 4);
setPrioritySignalsOverride("key", null);
setPrioritySignalsOverride("key", 5);
setPrioritySignalsOverride("key", 6);
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{{"key", 6}});
// Set multiple values.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key1", 1);
setPrioritySignalsOverride("key2");
setPrioritySignalsOverride("key3", -6);
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"null", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/
{{"key1", 1}, {"key2", absl::nullopt}, {"key3", -6}});
// Overrides should be respected when there's no bid.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:0, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key1", 1);
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{{"key1", 1}});
// Overrides should be respected when there's an invalid bid.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(R"({bid:1/0, render:"https://response.test/" })",
/*extra_code=*/R"(
setPrioritySignalsOverride("key1", 1);
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/ generateBid() bid of inf is not a valid bid."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{{"key1", 1}});
}
TEST_F(BidderWorkletTest, ReportWin) {
RunReportWinWithFunctionBodyExpectingResult(
"", /*expected_report_url =*/absl::nullopt);
RunReportWinWithFunctionBodyExpectingResult(
R"(return "https://ignored.test/")",
/*expected_report_url =*/absl::nullopt);
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("https://foo.test"))", GURL("https://foo.test/"));
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("https://foo.test/bar"))", GURL("https://foo.test/bar"));
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("http://http.not.allowed.test"))",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: sendReportTo must be passed a "
"valid HTTPS url."});
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("file:///file.not.allowed.test"))",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: sendReportTo must be passed a "
"valid HTTPS url."});
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo(""))", /*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: sendReportTo must be passed a "
"valid HTTPS url."});
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("https://foo.test");sendReportTo("https://foo.test"))",
/*expected_report_url =*/absl::nullopt, /*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: sendReportTo may be called at "
"most once."});
}
// Debug win/loss reporting APIs should do nothing when feature
// kBiddingAndScoringDebugReportingAPI is not enabled. It will not fail
// generateBid().
TEST_F(BidderWorkletTest, ForDebuggingOnlyReports) {
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"))"),
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionWin("https://win.url"))"),
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
}
TEST_F(BidderWorkletTest, DeleteBeforeReportWinCallback) {
AddJavascriptResponse(
&url_loader_factory_, interest_group_bidding_url_,
CreateReportWinScript(R"(sendReportTo("https://foo.test"))"));
auto bidder_worklet = CreateWorklet();
ASSERT_TRUE(bidder_worklet);
base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get());
bidder_worklet->ReportWin(
reporting_id_field_, reporting_id_, auction_signals_, per_buyer_signals_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, seller_signals_,
browser_signal_render_url_, browser_signal_bid_,
browser_signal_bid_currency_, browser_signal_highest_scoring_other_bid_,
browser_signal_highest_scoring_other_bid_currency_,
browser_signal_made_highest_scoring_other_bid_, browser_signal_ad_cost_,
browser_signal_modeling_signals_, browser_signal_join_count_,
browser_signal_recency_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, data_version_.value_or(0),
data_version_.has_value(),
/*trace_id=*/1,
base::BindOnce([](const absl::optional<GURL>& report_url,
const base::flat_map<std::string, GURL>& ad_beacon_map,
PrivateAggregationRequests pa_requests,
const std::vector<std::string>& errors) {
ADD_FAILURE() << "Callback should not be invoked since worklet deleted";
}));
base::RunLoop().RunUntilIdle();
bidder_worklet.reset();
event_handle->Signal();
}
// Test multiple ReportWin calls on a single worklet, in parallel. Do this
// twice, once before the worklet has loaded its Javascript, and once after, to
// make sure both cases work.
TEST_F(BidderWorkletTest, ReportWinParallel) {
// Each ReportWin call provides a different `auctionSignals` value. Use that
// in the report to verify that each call's values are plumbed through
// correctly.
const char kReportWinScript[] =
R"(sendReportTo("https://foo.test/" + auctionSignals))";
auto bidder_worklet = CreateWorklet();
// For the first loop iteration, call ReportWin repeatedly before providing
// the bidder script, then provide the bidder script. For the second loop
// iteration, reuse the bidder worklet from the first iteration, so the
// Javascript is loaded from the start.
for (bool report_win_invoked_before_worklet_script_loaded : {false, true}) {
SCOPED_TRACE(report_win_invoked_before_worklet_script_loaded);
base::RunLoop run_loop;
const size_t kNumReportWinCalls = 10;
size_t num_report_win_calls = 0;
for (size_t i = 0; i < kNumReportWinCalls; ++i) {
bidder_worklet->ReportWin(
reporting_id_field_, reporting_id_,
/*auction_signals_json=*/base::NumberToString(i), per_buyer_signals_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, seller_signals_,
browser_signal_render_url_, browser_signal_bid_,
browser_signal_bid_currency_,
browser_signal_highest_scoring_other_bid_,
browser_signal_highest_scoring_other_bid_currency_,
browser_signal_made_highest_scoring_other_bid_,
browser_signal_ad_cost_, browser_signal_modeling_signals_,
browser_signal_join_count_, browser_signal_recency_,
browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, data_version_.value_or(0),
data_version_.has_value(),
/*trace_id=*/1,
base::BindLambdaForTesting(
[&run_loop, &num_report_win_calls, i](
const absl::optional<GURL>& report_url,
const base::flat_map<std::string, GURL>& ad_beacon_map,
PrivateAggregationRequests pa_requests,
const std::vector<std::string>& errors) {
EXPECT_EQ(GURL(base::StringPrintf("https://foo.test/%zu", i)),
report_url);
EXPECT_TRUE(errors.empty());
++num_report_win_calls;
if (num_report_win_calls == kNumReportWinCalls) {
run_loop.Quit();
}
}));
}
// If this is the first loop iteration, wait for all the Mojo calls to
// settle, and then provide the Javascript response body.
if (report_win_invoked_before_worklet_script_loaded == false) {
task_environment_.RunUntilIdle();
EXPECT_FALSE(run_loop.AnyQuitCalled());
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateReportWinScript(kReportWinScript));
}
run_loop.Run();
EXPECT_EQ(kNumReportWinCalls, num_report_win_calls);
}
}
// Test multiple ReportWin calls on a single worklet, in parallel, in the case
// the worklet script fails to load.
TEST_F(BidderWorkletTest, ReportWinParallelLoadFails) {
auto bidder_worklet = CreateWorklet();
for (size_t i = 0; i < 10; ++i) {
bidder_worklet->ReportWin(
reporting_id_field_, reporting_id_,
/*auction_signals_json=*/base::NumberToString(i), per_buyer_signals_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, seller_signals_,
browser_signal_render_url_, browser_signal_bid_,
browser_signal_bid_currency_, browser_signal_highest_scoring_other_bid_,
browser_signal_highest_scoring_other_bid_currency_,
browser_signal_made_highest_scoring_other_bid_, browser_signal_ad_cost_,
browser_signal_modeling_signals_, browser_signal_join_count_,
browser_signal_recency_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, data_version_.value_or(0),
data_version_.has_value(),
/*trace_id=*/1,
base::BindOnce(
[](const absl::optional<GURL>& report_url,
const base::flat_map<std::string, GURL>& ad_beacon_map,
PrivateAggregationRequests pa_requests,
const std::vector<std::string>& errors) {
ADD_FAILURE() << "Callback should not be invoked.";
}));
}
url_loader_factory_.AddResponse(interest_group_bidding_url_.spec(),
CreateBasicGenerateBidScript(),
net::HTTP_NOT_FOUND);
EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
WaitForDisconnect());
}
// Make sure Date() is not available when running reportWin().
TEST_F(BidderWorkletTest, ReportWinDateNotAvailable) {
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("https://foo.test/" + Date().toString()))",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught ReferenceError: Date is not defined."});
}
TEST_F(BidderWorkletTest, ReportWinReportingId) {
const char kScriptBody[] = R"(
sendReportTo("https://example.org/?" +
browserSignals.interestGroupName + "/" +
browserSignals.buyerReportingId + "/" +
browserSignals.buyerAndSellerReportingId);
)";
reporting_id_ = "reporting_id";
reporting_id_field_ = mojom::ReportingIdField::kInterestGroupName;
RunReportWinWithFunctionBodyExpectingResult(
kScriptBody,
GURL("https://example.org/?reporting_id/undefined/undefined"));
reporting_id_field_ = mojom::ReportingIdField::kBuyerReportingId;
RunReportWinWithFunctionBodyExpectingResult(
kScriptBody,
GURL("https://example.org/?undefined/reporting_id/undefined"));
reporting_id_field_ = mojom::ReportingIdField::kBuyerAndSellerReportingId;
RunReportWinWithFunctionBodyExpectingResult(
kScriptBody,
GURL("https://example.org/?undefined/undefined/reporting_id"));
}
TEST_F(BidderWorkletTest, ReportWinDataVersion) {
data_version_ = 5u;
RunReportWinWithFunctionBodyExpectingResult(
"sendReportTo('https://dataVersion/'+browserSignals.dataVersion)",
GURL("https://dataVersion/5"));
}
// It shouldn't matter the order in which network fetches complete. For each
// required and optional reportWin() URL load prerequisite, ensure that
// reportWin() completes when that URL is the last loaded URL.
TEST_F(BidderWorkletTest, ReportWinLoadCompletionOrder) {
constexpr char kJsonResponse[] = "{}";
constexpr char kDirectFromSellerSignalsHeaders[] =
"X-Allow-FLEDGE: true\nX-FLEDGE-Auction-Only: true";
direct_from_seller_per_buyer_signals_ =
GURL("https://url.test/perbuyersignals");
direct_from_seller_auction_signals_ = GURL("https://url.test/auctionsignals");
struct Response {
GURL response_url;
std::string response_type;
std::string headers;
std::string content;
};
const Response kResponses[] = {
{interest_group_bidding_url_, kJavascriptMimeType, kAllowFledgeHeader,
CreateReportWinScript(R"(sendReportTo("https://foo.test"))")},
{*direct_from_seller_per_buyer_signals_, kJsonMimeType,
kDirectFromSellerSignalsHeaders, kJsonResponse},
{*direct_from_seller_auction_signals_, kJsonMimeType,
kDirectFromSellerSignalsHeaders, kJsonResponse}};
// Cycle such that each response in `kResponses` gets to be the last response,
// like so:
//
// 0,1,2
// 1,2,0
// 2,0,1
for (size_t offset = 0; offset < std::size(kResponses); ++offset) {
SCOPED_TRACE(offset);
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
url_loader_factory_.ClearResponses();
auto run_loop = std::make_unique<base::RunLoop>();
RunReportWinExpectingResultAsync(bidder_worklet.get(),
GURL("https://foo.test/"), {}, {}, {},
run_loop->QuitClosure());
for (size_t i = 0; i < std::size(kResponses); ++i) {
SCOPED_TRACE(i);
const Response& response =
kResponses[(i + offset) % std::size(kResponses)];
AddResponse(
&url_loader_factory_, response.response_url, response.response_type,
/*charset=*/absl::nullopt, response.content, response.headers);
task_environment_.RunUntilIdle();
if (i < std::size(kResponses) - 1) {
// Some URLs haven't finished loading -- generateBid() should be
// blocked.
EXPECT_FALSE(run_loop->AnyQuitCalled());
}
}
// The last URL for this generateBid() call has completed -- check that
// generateBid() returns.
run_loop->Run();
}
}
TEST_F(BidderWorkletTest, ReportWinAuctionSignals) {
// Non-JSON strings should silently result in failure generating the bid,
// before the result can be scored.
auction_signals_ = "https://interest.group.name.test/";
RunGenerateBidWithJavascriptExpectingResult(CreateBasicGenerateBidScript(),
mojom::BidderWorkletBidPtr());
auction_signals_ = R"("https://interest.group.name.test/")";
RunReportWinWithFunctionBodyExpectingResult(
"sendReportTo(auctionSignals)",
GURL("https://interest.group.name.test/"));
auction_signals_ = absl::nullopt;
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("https://" + (auctionSignals === null)))",
GURL("https://true/"));
}
TEST_F(BidderWorkletTest, ReportWinPerBuyerSignals) {
// Non-JSON strings should silently result in failure generating the bid,
// before the result can be scored.
per_buyer_signals_ = "https://interest.group.name.test/";
RunGenerateBidWithJavascriptExpectingResult(CreateBasicGenerateBidScript(),
mojom::BidderWorkletBidPtr());
per_buyer_signals_ = R"("https://interest.group.name.test/")";
RunReportWinWithFunctionBodyExpectingResult(
"sendReportTo(perBuyerSignals)",
GURL("https://interest.group.name.test/"));
per_buyer_signals_ = absl::nullopt;
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("https://" + (perBuyerSignals === null)))",
GURL("https://true/"));
}
TEST_F(BidderWorkletTest, ReportWinSellerSignals) {
// Non-JSON values should silently result in failures. This shouldn't happen,
// except in the case of a compromised seller worklet process, so not worth
// having an error message.
seller_signals_ = "https://interest.group.name.test/";
RunReportWinWithFunctionBodyExpectingResult(
"sendReportTo(sellerSignals)", /*expected_report_url=*/absl::nullopt);
seller_signals_ = R"("https://interest.group.name.test/")";
RunReportWinWithFunctionBodyExpectingResult(
"sendReportTo(sellerSignals)", GURL("https://interest.group.name.test/"));
}
TEST_F(BidderWorkletTest, ReportWinInterestGroupOwner) {
interest_group_bidding_url_ = GURL("https://foo.test/bar");
// Add an extra ".test" because origin's shouldn't have a terminal slash,
// unlike URLs. If an extra slash were added to the origin, this would end up
// as https://foo.test/.test, instead.
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo(browserSignals.interestGroupOwner + ".test"))",
GURL("https://foo.test.test/"));
interest_group_bidding_url_ = GURL("https://[::1]:40000/");
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo(browserSignals.interestGroupOwner))",
GURL("https://[::1]:40000/"));
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalTopWindowOrigin) {
top_window_origin_ = url::Origin::Create(GURL("https://top.window.test/"));
RunReportWinWithFunctionBodyExpectingResult(
R"(sendReportTo("https://" + browserSignals.topWindowHostname))",
GURL("https://top.window.test/"));
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalRenderUrl) {
browser_signal_render_url_ = GURL("https://shrimp.test/");
RunReportWinWithFunctionBodyExpectingResult(
"sendReportTo(browserSignals.renderUrl)", browser_signal_render_url_);
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalBid) {
browser_signal_bid_ = 4;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.bid === 4)
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalHighestScoringOtherBid) {
browser_signal_highest_scoring_other_bid_ = 3;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.highestScoringOtherBid === 3)
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalBidCurrency) {
browser_signal_bid_currency_ = absl::nullopt;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.bidCurrency === "???")
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
browser_signal_bid_currency_ = blink::AdCurrency::From("USD");
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.bidCurrency === "USD")
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
}
TEST_F(BidderWorkletTest,
ReportWinBrowserSignalHighestScoringOtherBidCurrency) {
browser_signal_highest_scoring_other_bid_currency_ =
blink::AdCurrency::From("CAD");
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.highestScoringOtherBidCurrency === "CAD")
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
browser_signal_highest_scoring_other_bid_currency_ = absl::nullopt;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.highestScoringOtherBidCurrency === "???")
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalIsHighestScoringOtherBidMe) {
browser_signal_made_highest_scoring_other_bid_ = true;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.madeHighestScoringOtherBid)
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalSeller) {
GURL seller_raw_url = GURL("https://seller.origin.test");
browser_signal_seller_origin_ = url::Origin::Create(seller_raw_url);
RunReportWinWithFunctionBodyExpectingResult(
"sendReportTo(browserSignals.seller)",
/*expected_report_url=*/seller_raw_url);
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalTopLevelSeller) {
browser_signal_top_level_seller_origin_ = absl::nullopt;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (!("topLevelSeller" in browserSignals))
sendReportTo('https://pass.test');)",
/*expected_report_url=*/GURL("https://pass.test"));
GURL top_level_seller_raw_url = GURL("https://top.level.seller.origin.test");
browser_signal_top_level_seller_origin_ =
url::Origin::Create(top_level_seller_raw_url);
RunReportWinWithFunctionBodyExpectingResult(
"sendReportTo(browserSignals.topLevelSeller)",
/*expected_report_url=*/top_level_seller_raw_url);
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalModelingSignals) {
browser_signal_modeling_signals_ = absl::nullopt;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (!("modelingSignals" in browserSignals))
sendReportTo('https://pass.test');)",
/*expected_report_url=*/GURL("https://pass.test"));
browser_signal_modeling_signals_ = 123u;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.modelingSignals === 123)
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalJoinCount) {
browser_signal_join_count_ = 7;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.joinCount === 7)
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
}
TEST_F(BidderWorkletTest, ReportWinBrowserSignalRecency) {
browser_signal_recency_ = 19u;
RunReportWinWithFunctionBodyExpectingResult(
R"(if (browserSignals.recency === 19)
sendReportTo("https://jumboshrimp.test"))",
GURL("https://jumboshrimp.test"));
}
// Subsequent runs of the same script should not affect each other. Same is true
// for different scripts, but it follows from the single script case.
//
// TODO(mmenke): The current API only allows each generateBid() method to be
// called once, but each ReportWin() to be called multiple times. When the API
// is updated to allow multiple calls to generateBid(), update this method to
// invoke it multiple times.
TEST_F(BidderWorkletTest, ScriptIsolation) {
// Use arrays so that all values are references, to catch both the case where
// variables are persisted, and the case where what they refer to is
// persisted, but variables are overwritten between runs.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
R"(
// Globally scoped variable.
if (!globalThis.var1)
globalThis.var1 = [1];
generateBid = function() {
// Value only visible within this closure.
var var2 = [2];
return function() {
return {
// Expose values in "ad" object.
ad: [++globalThis.var1[0], ++var2[0]],
bid: 1,
render:"https://response.test/"
}
};
}();
function reportWin() {
// Reuse generateBid() to check same potential cases for leaks between
// successive calls.
var ad = generateBid().ad;
sendReportTo("https://" + ad[0] + ad[1] + ".test/");
}
)");
auto bidder_worklet = CreateWorkletAndGenerateBid();
ASSERT_TRUE(bidder_worklet);
for (int i = 0; i < 3; ++i) {
base::RunLoop run_loop;
bidder_worklet->ReportWin(
reporting_id_field_, reporting_id_, auction_signals_,
per_buyer_signals_, direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, seller_signals_,
browser_signal_render_url_, browser_signal_bid_,
browser_signal_bid_currency_, browser_signal_highest_scoring_other_bid_,
browser_signal_highest_scoring_other_bid_currency_,
browser_signal_made_highest_scoring_other_bid_, browser_signal_ad_cost_,
browser_signal_modeling_signals_, browser_signal_join_count_,
browser_signal_recency_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, data_version_.value_or(0),
data_version_.has_value(),
/*trace_id=*/1,
base::BindLambdaForTesting(
[&run_loop](const absl::optional<GURL>& report_url,
const base::flat_map<std::string, GURL>& ad_beacon_map,
PrivateAggregationRequests pa_requests,
const std::vector<std::string>& errors) {
EXPECT_EQ(GURL("https://23.test/"), report_url);
EXPECT_TRUE(errors.empty());
run_loop.Quit();
}));
run_loop.Run();
}
}
TEST_F(BidderWorkletTest, PauseOnStart) {
// If pause isn't working, this will be used and not the right script.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
"nonsense;");
// No trusted signals to simplify spying on URL loading.
interest_group_trusted_bidding_signals_keys_.reset();
BidderWorklet* worklet_impl = nullptr;
auto worklet =
CreateWorklet(interest_group_bidding_url_,
/* pause_for_debugger_on_start=*/true, &worklet_impl);
GenerateBid(worklet.get());
// Grab the context group ID to be able to resume.
int id = worklet_impl->context_group_id_for_testing();
// Give it a chance to fetch.
task_environment_.RunUntilIdle();
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateBasicGenerateBidScript());
// Set up the event loop for the standard callback.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
// Let this run.
v8_helper_->v8_runner()->PostTask(
FROM_HERE, base::BindOnce([](scoped_refptr<AuctionV8Helper> v8_helper,
int id) { v8_helper->Resume(id); },
v8_helper_, id));
load_script_run_loop_->Run();
load_script_run_loop_.reset();
ASSERT_TRUE(bid_);
EXPECT_EQ("[\"ad\"]", bid_->ad);
EXPECT_EQ(1, bid_->bid);
EXPECT_EQ(GURL("https://response.test/"), bid_->ad_descriptor.url);
EXPECT_THAT(bid_errors_, ::testing::UnorderedElementsAre());
}
TEST_F(BidderWorkletTest, PauseOnStartDelete) {
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateBasicGenerateBidScript());
// No trusted signals to simplify things.
interest_group_trusted_bidding_signals_keys_.reset();
BidderWorklet* worklet_impl = nullptr;
auto worklet =
CreateWorklet(interest_group_bidding_url_,
/*pause_for_debugger_on_start=*/true, &worklet_impl);
GenerateBid(worklet.get());
// Give it a chance to fetch.
task_environment_.RunUntilIdle();
// Grab the context group ID.
int id = worklet_impl->context_group_id_for_testing();
// Delete the worklet. No callback should be invoked.
worklet.reset();
task_environment_.RunUntilIdle();
// Try to resume post-delete. Should not crash.
v8_helper_->v8_runner()->PostTask(
FROM_HERE, base::BindOnce([](scoped_refptr<AuctionV8Helper> v8_helper,
int id) { v8_helper->Resume(id); },
v8_helper_, id));
task_environment_.RunUntilIdle();
}
TEST_F(BidderWorkletTest, BasicV8Debug) {
ScopedInspectorSupport inspector_support(v8_helper_.get());
// Helper for looking for scriptParsed events.
auto is_script_parsed = [](const TestChannel::Event& event) -> bool {
if (event.type != TestChannel::Event::Type::Notification) {
return false;
}
const std::string* candidate_method =
event.value.GetDict().FindString("method");
return (candidate_method && *candidate_method == "Debugger.scriptParsed");
};
const char kUrl1[] = "http://example.com/first.js";
const char kUrl2[] = "http://example.org/second.js";
AddJavascriptResponse(&url_loader_factory_, GURL(kUrl1),
CreateBasicGenerateBidScript());
AddJavascriptResponse(&url_loader_factory_, GURL(kUrl2),
CreateBasicGenerateBidScript());
BidderWorklet* worklet_impl1;
auto worklet1 = CreateWorklet(
GURL(kUrl1), /*pause_for_debugger_on_start=*/true, &worklet_impl1);
GenerateBid(worklet1.get());
BidderWorklet* worklet_impl2;
auto worklet2 = CreateWorklet(
GURL(kUrl2), /*pause_for_debugger_on_start=*/true, &worklet_impl2);
GenerateBid(worklet2.get());
int id1 = worklet_impl1->context_group_id_for_testing();
int id2 = worklet_impl2->context_group_id_for_testing();
TestChannel* channel1 = inspector_support.ConnectDebuggerSession(id1);
TestChannel* channel2 = inspector_support.ConnectDebuggerSession(id2);
channel1->RunCommandAndWaitForResult(
1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})");
channel1->RunCommandAndWaitForResult(
2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
channel2->RunCommandAndWaitForResult(
1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})");
channel2->RunCommandAndWaitForResult(
2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
// Should not see scriptParsed before resume.
std::list<TestChannel::Event> events1 = channel1->TakeAllEvents();
EXPECT_TRUE(base::ranges::none_of(events1, is_script_parsed));
// Unpause execution for #1.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
channel1->RunCommandAndWaitForResult(
3, "Runtime.runIfWaitingForDebugger",
R"({"id":3,"method":"Runtime.runIfWaitingForDebugger","params":{}})");
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
bid_.reset();
load_script_run_loop_.reset();
// channel1 should have had a parsed notification for kUrl1.
TestChannel::Event script_parsed1 =
channel1->WaitForMethodNotification("Debugger.scriptParsed");
const std::string* url1 =
script_parsed1.value.GetDict().FindStringByDottedPath("params.url");
ASSERT_TRUE(url1);
EXPECT_EQ(kUrl1, *url1);
// There shouldn't be a parsed notification on channel 2, however.
std::list<TestChannel::Event> events2 = channel2->TakeAllEvents();
EXPECT_TRUE(base::ranges::none_of(events2, is_script_parsed));
// Unpause execution for #2.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
channel2->RunCommandAndWaitForResult(
3, "Runtime.runIfWaitingForDebugger",
R"({"id":3,"method":"Runtime.runIfWaitingForDebugger","params":{}})");
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
bid_.reset();
load_script_run_loop_.reset();
// channel2 should have had a parsed notification for kUrl2.
TestChannel::Event script_parsed2 =
channel2->WaitForMethodNotification("Debugger.scriptParsed");
const std::string* url2 =
script_parsed2.value.GetDict().FindStringByDottedPath("params.url");
ASSERT_TRUE(url2);
EXPECT_EQ(kUrl2, *url2);
worklet1.reset();
worklet2.reset();
task_environment_.RunUntilIdle();
// No other scriptParsed events should be on either channel.
events1 = channel1->TakeAllEvents();
events2 = channel2->TakeAllEvents();
EXPECT_TRUE(base::ranges::none_of(events1, is_script_parsed));
EXPECT_TRUE(base::ranges::none_of(events2, is_script_parsed));
}
TEST_F(BidderWorkletTest, ParseErrorV8Debug) {
ScopedInspectorSupport inspector_support(v8_helper_.get());
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
"Invalid Javascript");
BidderWorklet* worklet_impl = nullptr;
auto worklet =
CreateWorklet(interest_group_bidding_url_,
/*pause_for_debugger_on_start=*/true, &worklet_impl);
GenerateBidExpectingNeverCompletes(worklet.get());
int id = worklet_impl->context_group_id_for_testing();
TestChannel* channel = inspector_support.ConnectDebuggerSession(id);
channel->RunCommandAndWaitForResult(
1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})");
channel->RunCommandAndWaitForResult(
2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
// Unpause execution.
channel->RunCommandAndWaitForResult(
3, "Runtime.runIfWaitingForDebugger",
R"({"id":3,"method":"Runtime.runIfWaitingForDebugger","params":{}})");
// Worklet should disconnect with an error message.
EXPECT_FALSE(WaitForDisconnect().empty());
// Should have gotten a parse error notification.
TestChannel::Event parse_error =
channel->WaitForMethodNotification("Debugger.scriptFailedToParse");
const std::string* error_url =
parse_error.value.GetDict().FindStringByDottedPath("params.url");
ASSERT_TRUE(error_url);
EXPECT_EQ(interest_group_bidding_url_.spec(), *error_url);
}
TEST_F(BidderWorkletTest, BasicDevToolsDebug) {
std::string bid_script = CreateGenerateBidScript(
R"({ad: ["ad"], bid: this.global_bid ? this.global_bid : 1,
render:"https://response.test/"})");
const char kUrl1[] = "http://example.com/first.js";
const char kUrl2[] = "http://example.org/second.js";
AddJavascriptResponse(&url_loader_factory_, GURL(kUrl1), bid_script);
AddJavascriptResponse(&url_loader_factory_, GURL(kUrl2), bid_script);
auto worklet1 =
CreateWorklet(GURL(kUrl1), /*pause_for_debugger_on_start=*/true);
GenerateBid(worklet1.get());
auto worklet2 =
CreateWorklet(GURL(kUrl2), /*pause_for_debugger_on_start=*/true);
GenerateBid(worklet2.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent1, agent2;
worklet1->ConnectDevToolsAgent(agent1.BindNewEndpointAndPassReceiver());
worklet2->ConnectDevToolsAgent(agent2.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug1(std::move(agent1), "123",
/*use_binary_protocol=*/true);
TestDevToolsAgentClient debug2(std::move(agent2), "456",
/*use_binary_protocol=*/true);
debug1.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug1.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
debug2.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug2.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
const char kBreakpointTemplate[] = R"({
"id":3,
"method":"Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 0,
"url": "%s",
"columnNumber": 0,
"condition": ""
}})";
debug1.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 3, "Debugger.setBreakpointByUrl",
base::StringPrintf(kBreakpointTemplate, kUrl1));
debug2.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 3, "Debugger.setBreakpointByUrl",
base::StringPrintf(kBreakpointTemplate, kUrl2));
// Now start #1. We should see a scriptParsed event.
debug1.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 4,
"Runtime.runIfWaitingForDebugger",
R"({"id":4,"method":"Runtime.runIfWaitingForDebugger","params":{}})");
TestDevToolsAgentClient::Event script_parsed1 =
debug1.WaitForMethodNotification("Debugger.scriptParsed");
const std::string* url1 =
script_parsed1.value.GetDict().FindStringByDottedPath("params.url");
ASSERT_TRUE(url1);
EXPECT_EQ(*url1, kUrl1);
// Next there is the breakpoint.
TestDevToolsAgentClient::Event breakpoint_hit1 =
debug1.WaitForMethodNotification("Debugger.paused");
base::Value::List* hit_breakpoints1 =
breakpoint_hit1.value.GetDict().FindListByDottedPath(
"params.hitBreakpoints");
ASSERT_TRUE(hit_breakpoints1);
ASSERT_EQ(1u, hit_breakpoints1->size());
ASSERT_TRUE((*hit_breakpoints1)[0].is_string());
EXPECT_EQ("1:0:0:http://example.com/first.js",
(*hit_breakpoints1)[0].GetString());
std::string* callframe_id1 = breakpoint_hit1.value.GetDict()
.FindDict("params")
->FindList("callFrames")
->front()
.GetDict()
.FindString("callFrameId");
// Override the bid value.
const char kCommandTemplate[] = R"({
"id": 5,
"method": "Debugger.evaluateOnCallFrame",
"params": {
"callFrameId": "%s",
"expression": "global_bid = 42"
}
})";
debug1.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 5, "Debugger.evaluateOnCallFrame",
base::StringPrintf(kCommandTemplate, callframe_id1->c_str()));
// Resume, setting up event loop for fixture first.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
debug1.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 6, "Debugger.resume",
R"({"id":6,"method":"Debugger.resume","params":{}})");
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(42, bid_->bid);
bid_.reset();
// Start #2, see that it hit its breakpoint.
debug2.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 4,
"Runtime.runIfWaitingForDebugger",
R"({"id":4,"method":"Runtime.runIfWaitingForDebugger","params":{}})");
TestDevToolsAgentClient::Event breakpoint_hit2 =
debug2.WaitForMethodNotification("Debugger.paused");
base::Value::List* hit_breakpoints2 =
breakpoint_hit2.value.GetDict().FindListByDottedPath(
"params.hitBreakpoints");
ASSERT_TRUE(hit_breakpoints2);
ASSERT_EQ(1u, hit_breakpoints2->size());
ASSERT_TRUE((*hit_breakpoints2)[0].is_string());
EXPECT_EQ("1:0:0:http://example.org/second.js",
(*hit_breakpoints2)[0].GetString());
// Go ahead and resume w/o messing with anything.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
debug2.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 5, "Debugger.resume",
R"({"id":5,"method":"Debugger.resume","params":{}})");
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
}
TEST_F(BidderWorkletTest, InstrumentationBreakpoints) {
const char kUrl[] = "http://example.com/bid.js";
AddJavascriptResponse(
&url_loader_factory_, GURL(kUrl),
CreateReportWinScript(R"(sendReportTo("https://foo.test"))"));
auto worklet =
CreateWorklet(GURL(kUrl), /*pause_for_debugger_on_start=*/true);
GenerateBid(worklet.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent;
worklet->ConnectDevToolsAgent(agent.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug(std::move(agent), "123",
/*use_binary_protocol=*/true);
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
// Set the instrumentation breakpoints.
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 3,
"EventBreakpoints.setInstrumentationBreakpoint",
MakeInstrumentationBreakpointCommand(3, "set",
"beforeBidderWorkletBiddingStart"));
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 4,
"EventBreakpoints.setInstrumentationBreakpoint",
MakeInstrumentationBreakpointCommand(
4, "set", "beforeBidderWorkletReportingStart"));
// Unpause the execution. We should immediately hit the breakpoint right
// after since BidderWorklet does bidding on creation.
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 5,
"Runtime.runIfWaitingForDebugger",
R"({"id":5,"method":"Runtime.runIfWaitingForDebugger","params":{}})");
TestDevToolsAgentClient::Event breakpoint_hit1 =
debug.WaitForMethodNotification("Debugger.paused");
const std::string* breakpoint1 =
breakpoint_hit1.value.GetDict().FindStringByDottedPath(
"params.data.eventName");
ASSERT_TRUE(breakpoint1);
EXPECT_EQ("instrumentation:beforeBidderWorkletBiddingStart", *breakpoint1);
// Resume and wait for bid result.
load_script_run_loop_ = std::make_unique<base::RunLoop>();
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 6, "Debugger.resume",
R"({"id":6,"method":"Debugger.resume","params":{}})");
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
bid_.reset();
// Now ask for reporting. This should hit the other breakpoint.
base::RunLoop run_loop;
RunReportWinExpectingResultAsync(worklet.get(), GURL("https://foo.test/"), {},
{}, {}, run_loop.QuitClosure());
TestDevToolsAgentClient::Event breakpoint_hit2 =
debug.WaitForMethodNotification("Debugger.paused");
const std::string* breakpoint2 =
breakpoint_hit2.value.GetDict().FindStringByDottedPath(
"params.data.eventName");
ASSERT_TRUE(breakpoint2);
EXPECT_EQ("instrumentation:beforeBidderWorkletReportingStart", *breakpoint2);
// Resume and wait for reporting to finish.
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 7, "Debugger.resume",
R"({"id":7,"method":"Debugger.resume","params":{}})");
run_loop.Run();
}
TEST_F(BidderWorkletTest, UnloadWhilePaused) {
// Make sure things are cleaned up properly if the worklet is destroyed while
// paused on a breakpoint.
const char kUrl[] = "http://example.com/bid.js";
AddJavascriptResponse(
&url_loader_factory_, GURL(kUrl),
CreateReportWinScript(R"(sendReportTo("https://foo.test"))"));
auto worklet =
CreateWorklet(GURL(kUrl), /*pause_for_debugger_on_start=*/true);
GenerateBid(worklet.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent;
worklet->ConnectDevToolsAgent(agent.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug(std::move(agent), "123",
/*use_binary_protocol=*/true);
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
// Set an instrumentation breakpoint to get it debugger paused.
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 3,
"EventBreakpoints.setInstrumentationBreakpoint",
MakeInstrumentationBreakpointCommand(3, "set",
"beforeBidderWorkletBiddingStart"));
debug.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 4,
"Runtime.runIfWaitingForDebugger",
R"({"id":4,"method":"Runtime.runIfWaitingForDebugger","params":{}})");
// Wait for breakpoint to hit.
debug.WaitForMethodNotification("Debugger.paused");
// ... and destroy the worklet
worklet.reset();
// This won't terminate if the V8 thread is still blocked in debugger.
task_environment_.RunUntilIdle();
}
TEST_F(BidderWorkletTest, ExecutionModeGroupByOrigin) {
const char kScript[] = R"(
if (!('count' in globalThis))
globalThis.count = 0;
function generateBid() {
++count;
return {ad: ["ad"], bid:count, render:"https://response.test/"};
}
)";
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
kScript);
// Run 1, start group.
execution_mode_ =
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode;
join_origin_ = url::Origin::Create(GURL("https://url.test/"));
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
// Run 2, same group.
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(2, bid_->bid);
// Run 3, not in group.
execution_mode_ =
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode;
join_origin_ = url::Origin::Create(GURL("https://url2.test/"));
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
// Run 4, back to group.
execution_mode_ =
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode;
join_origin_ = url::Origin::Create(GURL("https://url.test/"));
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(3, bid_->bid);
// Run 5, different group.
join_origin_ = url::Origin::Create(GURL("https://url2.test/"));
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
// Run 5, different group cont'd.
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(2, bid_->bid);
}
TEST_F(BidderWorkletTest, ExecutionModeFrozenContext) {
const char kScript[] = R"(
if (!('count' in globalThis))
globalThis.count = 1;
function generateBid() {
++count; // This increment is a no-op if the context is frozen.
return {
ad: ["ad"],
bid: count,
render:"https://response.test/?" + Object.isFrozen(globalThis)};
}
)";
std::string frozen_url = "https://response.test/?true";
std::string not_frozen_url = "https://response.test/?false";
interest_group_ads_.emplace_back(GURL(frozen_url),
/*metadata=*/absl::nullopt);
interest_group_ads_.emplace_back(GURL(not_frozen_url),
/*metadata=*/absl::nullopt);
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
kScript);
// Run 1, frozen.
execution_mode_ = blink::mojom::InterestGroup::ExecutionMode::kFrozenContext;
join_origin_ = url::Origin::Create(GURL("https://url.test/"));
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
EXPECT_THAT(bid_errors_, testing::ElementsAre());
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
EXPECT_EQ(frozen_url, bid_->ad_descriptor.url);
// Run 2, frozen.
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
EXPECT_EQ(frozen_url, bid_->ad_descriptor.url);
// Run 3, grouped.
execution_mode_ =
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode;
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(2, bid_->bid);
EXPECT_EQ(not_frozen_url, bid_->ad_descriptor.url);
// Run 4, grouped.
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(3, bid_->bid);
EXPECT_EQ(not_frozen_url, bid_->ad_descriptor.url);
// Run 5, frozen.
execution_mode_ = blink::mojom::InterestGroup::ExecutionMode::kFrozenContext;
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
EXPECT_EQ(frozen_url, bid_->ad_descriptor.url);
// Run 6, compatibility
execution_mode_ =
blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode;
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(2, bid_->bid);
EXPECT_EQ(not_frozen_url, bid_->ad_descriptor.url);
// Run 7, frozen
execution_mode_ = blink::mojom::InterestGroup::ExecutionMode::kFrozenContext;
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(1, bid_->bid);
EXPECT_EQ(frozen_url, bid_->ad_descriptor.url);
}
TEST_F(BidderWorkletTest, ExecutionModeFrozenContextFails) {
const char kScript[] = R"(
const incrementer = (function() {
let a = 1;
return function() { a += 1; return a; };
})();
function generateBid() {
return {ad: ["ad"], bid:incrementer(), render:"https://response.test/"};
}
)";
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
kScript);
execution_mode_ = blink::mojom::InterestGroup::ExecutionMode::kFrozenContext;
join_origin_ = url::Origin::Create(GURL("https://url.test/"));
GenerateBid(bidder_worklet.get());
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
EXPECT_THAT(bid_errors_,
testing::ElementsAre("undefined:0 Uncaught TypeError: Cannot "
"DeepFreeze non-const value a."));
EXPECT_FALSE(bid_);
}
// Test that cancelling the worklet before it runs but after the execution was
// queued actually cancels the execution. This is done by trying to run a
// while(true) {} script with a timeout that's bigger than the test timeout, so
// if it doesn't get cancelled the *test* will timeout.
TEST_F(BidderWorkletTest, Cancelation) {
per_buyer_timeout_ = base::Days(360);
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
"while(true) {}");
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
// Let the script load.
task_environment_.RunUntilIdle();
// Now we no longer need it for parsing JS, wedge the V8 thread so we get a
// chance to cancel the script *before* it actually tries running.
base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get());
GenerateBidClientWithCallbacks client(
GenerateBidClientWithCallbacks::GenerateBidNeverInvokedCallback());
mojo::AssociatedReceiver<mojom::GenerateBidClient> client_receiver(&client);
GenerateBid(bidder_worklet.get(),
client_receiver.BindNewEndpointAndPassRemote());
// Spin the event loop to let the signals negotiation go through. This is
// for this thread only since the V8 thread is wedged, but it's enough since
// all mojo objects live here. This should queue the V8 thread task for
// execution.
base::RunLoop().RunUntilIdle();
// Cancel and then unwedge.
client_receiver.reset();
base::RunLoop().RunUntilIdle();
event_handle->Signal();
// Make sure cancellation happens before ~BidderWorklet.
task_environment_.RunUntilIdle();
}
// Test that queued tasks get cancelled at worklet destruction.
TEST_F(BidderWorkletTest, CancelationDtor) {
per_buyer_timeout_ = base::Days(360);
// ReportWin timeout isn't configurable the way generateBid is.
v8_helper_->v8_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](scoped_refptr<AuctionV8Helper> v8_helper) {
v8_helper->set_script_timeout_for_testing(base::Days(360));
},
v8_helper_));
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
"while(true) {}");
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
// Let the script load.
task_environment_.RunUntilIdle();
// Now we no longer need it for parsing JS, wedge the V8 thread so we get a
// chance to cancel the script *before* it actually tries running.
base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get());
GenerateBid(bidder_worklet.get());
bidder_worklet->ReportWin(
reporting_id_field_, reporting_id_, auction_signals_, per_buyer_signals_,
direct_from_seller_per_buyer_signals_,
direct_from_seller_auction_signals_, seller_signals_,
browser_signal_render_url_, browser_signal_bid_,
browser_signal_bid_currency_, browser_signal_highest_scoring_other_bid_,
browser_signal_highest_scoring_other_bid_currency_,
browser_signal_made_highest_scoring_other_bid_, browser_signal_ad_cost_,
browser_signal_modeling_signals_, browser_signal_join_count_,
browser_signal_recency_, browser_signal_seller_origin_,
browser_signal_top_level_seller_origin_, data_version_.value_or(0),
data_version_.has_value(),
/*trace_id=*/1,
base::BindOnce([](const absl::optional<GURL>& report_url,
const base::flat_map<std::string, GURL>& ad_beacon_map,
PrivateAggregationRequests pa_requests,
const std::vector<std::string>& errors) {
ADD_FAILURE() << "Callback should not be invoked.";
}));
// Spin the event loop to let the signals negotiation go through. This is
// for this thread only since the V8 thread is wedged, but it's enough since
// all mojo objects live here. This should queue the V8 thread task for
// execution.
base::RunLoop().RunUntilIdle();
// Destroy the worklet, then unwedge.
bidder_worklet.reset();
base::RunLoop().RunUntilIdle();
event_handle->Signal();
}
// Test that cancelling execution before the script is fetched doesn't run it.
TEST_F(BidderWorkletTest, CancelBeforeFetch) {
per_buyer_timeout_ = base::Days(360);
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
GenerateBidClientWithCallbacks client(
GenerateBidClientWithCallbacks::GenerateBidNeverInvokedCallback());
mojo::AssociatedReceiver<mojom::GenerateBidClient> client_receiver(&client);
GenerateBid(bidder_worklet.get(),
client_receiver.BindNewEndpointAndPassRemote());
// Let them talk about trusted signals.
task_environment_.RunUntilIdle();
// Cancel and then make the script available.
client_receiver.reset();
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
"while (true) {}");
// Make sure cancellation happens before ~BidderWorklet.
task_environment_.RunUntilIdle();
}
class BidderWorkletBiddingAndScoringDebugReportingAPIEnabledTest
: public BidderWorkletTest {
public:
BidderWorkletBiddingAndScoringDebugReportingAPIEnabledTest() {
feature_list_.InitAndEnableFeature(
blink::features::kBiddingAndScoringDebugReportingAPI);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
// Test forDebuggingOnly.reportAdAuctionLoss() and
// forDebuggingOnly.reportAdAuctionWin() called in generateBid().
TEST_F(BidderWorkletBiddingAndScoringDebugReportingAPIEnabledTest,
ForDebuggingOnlyReports) {
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url");
forDebuggingOnly.reportAdAuctionWin("https://win.url"))"),
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{}, GURL("https://loss.url"),
GURL("https://win.url"));
// It's OK to call one API but not the other.
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"))"),
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{}, GURL("https://loss.url"),
/*expected_debug_win_report_url=*/absl::nullopt);
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionWin("https://win.url"))"),
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{}, /*expected_debug_loss_report_url=*/absl::nullopt,
GURL("https://win.url"));
}
TEST_F(BidderWorkletBiddingAndScoringDebugReportingAPIEnabledTest,
ForDebuggingOnlyReportsInvalidParameter) {
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionLoss(null))"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:5 Uncaught TypeError: "
"reportAdAuctionLoss requires 1 string parameter."});
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionWin([5]))"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:5 Uncaught TypeError: "
"reportAdAuctionWin requires 1 string parameter."});
std::vector<std::string> non_https_urls = {"http://report.url",
"file:///foo/", "Not a URL"};
for (const auto& url : non_https_urls) {
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(base::StringPrintf(
R"(forDebuggingOnly.reportAdAuctionLoss("%s"))", url.c_str())),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:5 Uncaught TypeError: "
"reportAdAuctionLoss must be passed a valid HTTPS url."});
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(base::StringPrintf(
R"(forDebuggingOnly.reportAdAuctionWin("%s"))", url.c_str())),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:5 Uncaught TypeError: "
"reportAdAuctionWin must be passed a valid HTTPS url."});
}
// No message if caught, but still no debug report URLs.
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(try {forDebuggingOnly.reportAdAuctionLoss("http://loss.url")}
catch (e) {})"),
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{}, /*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt);
}
TEST_F(BidderWorkletBiddingAndScoringDebugReportingAPIEnabledTest,
ForDebuggingOnlyReportsMultiCallsAllowed) {
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url");
forDebuggingOnly.reportAdAuctionLoss("https://loss.url2"))"),
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{}, GURL("https://loss.url2"),
/*expected_debug_win_report_url=*/absl::nullopt);
RunGenerateBidWithJavascriptExpectingResult(
CreateBasicGenerateBidScriptWithDebuggingReport(
R"(forDebuggingOnly.reportAdAuctionWin("https://win.url");
forDebuggingOnly.reportAdAuctionWin("https://win.url2"))"),
mojom::BidderWorkletBid::New(
"[\"ad\"]", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
GURL("https://win.url2"));
}
TEST_F(BidderWorkletBiddingAndScoringDebugReportingAPIEnabledTest,
GenerateBidHasError) {
// The bidding script has an error statement and the script will fail. But
// th loss report URLs before bidding script encounters error should be kept.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/"[\"ad\"]",
R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url1");
error;
forDebuggingOnly.reportAdAuctionLoss("https://loss.url2"))"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/:6 Uncaught ReferenceError: error is not defined."},
GURL("https://loss.url1"));
}
TEST_F(BidderWorkletBiddingAndScoringDebugReportingAPIEnabledTest,
GenerateBidInvalidReturnValue) {
// Keep debugging loss report URLs when generateBid() returns invalid
// value type.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:"invalid", render:"https://response.test/"})",
R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url");
forDebuggingOnly.reportAdAuctionWin("https://win.url"))"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ generateBid() returned object must have numeric bid "
"field."},
GURL("https://loss.url"),
/*expected_debug_win_report_url=*/absl::nullopt);
}
// Loss report URLs before bidding script times out should be kept.
TEST_F(BidderWorkletBiddingAndScoringDebugReportingAPIEnabledTest,
GenerateBidTimedOut) {
// The bidding script has an endless while loop. It will time out due to
// AuctionV8Helper's default script timeout (50 ms).
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
/*raw_return_value=*/"[\"ad\"]",
R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url1");
while (1);
forDebuggingOnly.reportAdAuctionLoss("https://loss.url2"))"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
{"https://url.test/ execution of `generateBid` timed out."},
GURL("https://loss.url1"));
}
TEST_F(BidderWorkletTest, ReportWinRegisterAdBeacon) {
base::flat_map<std::string, GURL> expected_ad_beacon_map = {
{"click", GURL("https://click.example.com/")},
{"view", GURL("https://view.example.com/")},
};
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon({
'click': "https://click.example.com/",
'view': "https://view.example.com/",
}))",
/*expected_report_url =*/absl::nullopt, expected_ad_beacon_map);
// Don't call twice.
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon({
'click': "https://click.example.com/",
'view': "https://view.example.com/",
});
registerAdBeacon())",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:15 Uncaught TypeError: registerAdBeacon may be "
"called at most once."});
// If called twice and the error is caught, use the first result.
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon({
'click': "https://click.example.com/",
'view': "https://view.example.com/",
});
try { registerAdBeacon() }
catch (e) {})",
/*expected_report_url =*/absl::nullopt, expected_ad_beacon_map);
// If error on first call, can be called again.
RunReportWinWithFunctionBodyExpectingResult(
R"(try { registerAdBeacon() }
catch (e) {}
registerAdBeacon({
'click': "https://click.example.com/",
'view': "https://view.example.com/",
}))",
/*expected_report_url =*/absl::nullopt, expected_ad_beacon_map);
// Error if no parameters
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon())",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: registerAdBeacon requires 1 "
"object parameter."});
// Error if parameter is not an object
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon("foo"))",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: registerAdBeacon requires 1 "
"object parameter."});
// Error if parameter is not an object
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon("foo"))",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: registerAdBeacon requires 1 "
"object parameter."});
// Error if parameter attributes are not strings
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon({
'click': "https://click.example.com/",
1: "https://view.example.com/",
}))",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: registerAdBeacon object "
"attributes must be strings."});
// Error if invalid reporting URL
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon({
'click': "https://click.example.com/",
'view': "gopher://view.example.com/",
}))",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: registerAdBeacon invalid "
"reporting url for key 'view': 'gopher://view.example.com/'."});
// Error if not trustworthy reporting URL
RunReportWinWithFunctionBodyExpectingResult(
R"(registerAdBeacon({
'click': "https://127.0.0.1/",
'view': "http://view.example.com/",
}))",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{},
/*expected_pa_requests=*/{},
{"https://url.test/:11 Uncaught TypeError: registerAdBeacon invalid "
"reporting url for key 'view': 'http://view.example.com/'."});
}
class BidderWorkletSharedStorageAPIDisabledTest : public BidderWorkletTest {
public:
BidderWorkletSharedStorageAPIDisabledTest() {
feature_list_.InitAndDisableFeature(blink::features::kSharedStorageAPI);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(BidderWorkletSharedStorageAPIDisabledTest, SharedStorageNotExposed) {
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.clear();
)"),
/*expected_bid=*/nullptr,
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught ReferenceError: sharedStorage is not "
"defined."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
RunReportWinWithFunctionBodyExpectingResult(
R"(
sharedStorage.clear();
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, /*expected_pa_requests=*/{},
/*expected_errors=*/
{"https://url.test/:12 Uncaught ReferenceError: sharedStorage is not "
"defined."});
}
class BidderWorkletSharedStorageAPIEnabledTest : public BidderWorkletTest {
public:
BidderWorkletSharedStorageAPIEnabledTest() {
feature_list_.InitAndEnableFeature(blink::features::kSharedStorageAPI);
}
protected:
base::test::ScopedFeatureList feature_list_;
};
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageWriteInGenerateBid) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
{
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_host_remote_ = receiver.BindNewPipeAndPassRemote();
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.set('a', 'b');
sharedStorage.set('a', 'b', {ignoreIfPresent: true});
sharedStorage.append('a', 'b');
sharedStorage.delete('a');
sharedStorage.clear();
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
// Make sure the shared storage mojom methods are invoked as they use a
// dedicated pipe.
task_environment_.RunUntilIdle();
using RequestType =
auction_worklet::TestAuctionSharedStorageHost::RequestType;
using Request = auction_worklet::TestAuctionSharedStorageHost::Request;
EXPECT_THAT(test_shared_storage_host.observed_requests(),
testing::ElementsAre(Request{.type = RequestType::kSet,
.key = u"a",
.value = u"b",
.ignore_if_present = false},
Request{.type = RequestType::kSet,
.key = u"a",
.value = u"b",
.ignore_if_present = true},
Request{.type = RequestType::kAppend,
.key = u"a",
.value = u"b",
.ignore_if_present = false},
Request{.type = RequestType::kDelete,
.key = u"a",
.value = std::u16string(),
.ignore_if_present = false},
Request{.type = RequestType::kClear,
.key = std::u16string(),
.value = std::u16string(),
.ignore_if_present = false}));
}
{
shared_storage_host_remote_ =
mojo::PendingRemote<mojom::AuctionSharedStorageHost>();
// Set the shared-storage permissions policy to disallowed.
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/true,
/*shared_storage_allowed=*/false);
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
sharedStorage.clear();
)"),
/*expected_bid=*/nullptr,
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: The \"shared-storage\" "
"Permissions Policy denied the method on sharedStorage."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/true,
/*shared_storage_allowed=*/true);
}
}
TEST_F(BidderWorkletSharedStorageAPIEnabledTest,
SharedStorageWriteInReportWin) {
auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host;
{
mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver(
&test_shared_storage_host);
shared_storage_host_remote_ = receiver.BindNewPipeAndPassRemote();
RunReportWinWithFunctionBodyExpectingResult(
R"(
sharedStorage.set('a', 'b');
sharedStorage.set('a', 'b', {ignoreIfPresent: true});
sharedStorage.append('a', 'b');
sharedStorage.delete('a');
sharedStorage.clear();
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, /*expected_pa_requests=*/{},
/*expected_errors=*/{});
// Make sure the shared storage mojom methods are invoked as they use a
// dedicated pipe.
task_environment_.RunUntilIdle();
using RequestType =
auction_worklet::TestAuctionSharedStorageHost::RequestType;
using Request = auction_worklet::TestAuctionSharedStorageHost::Request;
EXPECT_THAT(test_shared_storage_host.observed_requests(),
testing::ElementsAre(Request{.type = RequestType::kSet,
.key = u"a",
.value = u"b",
.ignore_if_present = false},
Request{.type = RequestType::kSet,
.key = u"a",
.value = u"b",
.ignore_if_present = true},
Request{.type = RequestType::kAppend,
.key = u"a",
.value = u"b",
.ignore_if_present = false},
Request{.type = RequestType::kDelete,
.key = u"a",
.value = std::u16string(),
.ignore_if_present = false},
Request{.type = RequestType::kClear,
.key = std::u16string(),
.value = std::u16string(),
.ignore_if_present = false}));
}
{
shared_storage_host_remote_ =
mojo::PendingRemote<mojom::AuctionSharedStorageHost>();
// Set the shared-storage permissions policy to disallowed.
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/true,
/*shared_storage_allowed=*/false);
RunReportWinWithFunctionBodyExpectingResult(
R"(
sharedStorage.clear();
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, /*expected_pa_requests=*/{},
/*expected_errors=*/
{"https://url.test/:12 Uncaught TypeError: The \"shared-storage\" "
"Permissions Policy denied the method on sharedStorage."});
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/true,
/*shared_storage_allowed=*/true);
}
}
class BidderWorkletPrivateAggregationEnabledTest : public BidderWorkletTest {
public:
BidderWorkletPrivateAggregationEnabledTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kPrivateAggregationApi,
{{"fledge_extensions_enabled", "true"}});
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(BidderWorkletPrivateAggregationEnabledTest, GenerateBid) {
mojom::PrivateAggregationRequest kExpectedRequest1(
mojom::AggregatableReportContribution::NewHistogramContribution(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123,
/*value=*/45)),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
mojom::PrivateAggregationRequest kExpectedRequest2(
mojom::AggregatableReportContribution::NewHistogramContribution(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/absl::MakeInt128(/*high=*/1,
/*low=*/0),
/*value=*/1)),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
mojom::PrivateAggregationRequest kExpectedForEventRequest1(
mojom::AggregatableReportContribution::NewForEventContribution(
mojom::AggregatableReportForEventContribution::New(
/*bucket=*/mojom::ForEventSignalBucket::NewIdBucket(234),
/*value=*/mojom::ForEventSignalValue::NewIntValue(56),
/*event_type=*/"reserved.win")),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
mojom::PrivateAggregationRequest kExpectedForEventRequest2(
mojom::AggregatableReportContribution::NewForEventContribution(
mojom::AggregatableReportForEventContribution::New(
/*bucket=*/mojom::ForEventSignalBucket::NewIdBucket(
absl::MakeInt128(/*high=*/1,
/*low=*/0)),
/*value=*/mojom::ForEventSignalValue::NewIntValue(2),
/*event_type=*/"reserved.win")),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
// Only sendHistogramReport() is called.
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest1.Clone());
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
std::move(expected_pa_requests));
}
// Only reportContributionForEvent() is called.
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedForEventRequest1.Clone());
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 234n, value: 56});
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
std::move(expected_pa_requests));
}
// Both sendHistogramReport() and reportContributionForEvent() are called.
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest1.Clone());
expected_pa_requests.push_back(kExpectedForEventRequest1.Clone());
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 234n, value: 56});
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
std::move(expected_pa_requests));
}
// Set the private-aggregation permissions policy to disallowed.
{
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/false,
/*shared_storage_allowed=*/true);
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
)"),
/*expected_bid=*/nullptr,
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught TypeError: The \"private-aggregation\" "
"Permissions Policy denied the method on privateAggregation."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/true,
/*shared_storage_allowed=*/true);
}
// Large bucket
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest2.Clone());
expected_pa_requests.push_back(kExpectedForEventRequest2.Clone());
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.sendHistogramReport(
{bucket: 18446744073709551616n, value: 1});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 18446744073709551616n, value: 2});
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
std::move(expected_pa_requests));
}
// Multiple requests
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest1.Clone());
expected_pa_requests.push_back(kExpectedRequest2.Clone());
expected_pa_requests.push_back(kExpectedForEventRequest1.Clone());
expected_pa_requests.push_back(kExpectedForEventRequest2.Clone());
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.sendHistogramReport(
{bucket: 18446744073709551616n, value: 1});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 234n, value: 56});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 18446744073709551616n, value: 2});
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
std::move(expected_pa_requests));
}
// An unrelated exception after sendHistogramReport and
// reportContributionForEvent shouldn't block the reports.
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest1.Clone());
expected_pa_requests.push_back(kExpectedForEventRequest1.Clone());
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 234n, value: 56});
error;
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/:9 Uncaught ReferenceError: error is not defined."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
std::move(expected_pa_requests));
}
// Debug mode enabled with debug key
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
kExpectedRequest1.contribution->Clone(),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, blink::mojom::DebugKey::New(1234u))));
expected_pa_requests.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
kExpectedForEventRequest1.contribution->Clone(),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, blink::mojom::DebugKey::New(1234u))));
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.enableDebugMode({debug_key: 1234n});
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 234n, value: 56});
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
std::move(expected_pa_requests));
}
// Debug mode enabled without debug key, but with multiple requests
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
kExpectedRequest1.contribution->Clone(),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, /*debug_key=*/nullptr)));
expected_pa_requests.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
kExpectedRequest2.contribution->Clone(),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, /*debug_key=*/nullptr)));
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.enableDebugMode();
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.sendHistogramReport(
{bucket: 18446744073709551616n, value: 1});
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
std::move(expected_pa_requests));
}
// Debug mode enabled twice
{
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.enableDebugMode();
privateAggregation.enableDebugMode();
)"),
/*expected_bid=*/
mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/:7 Uncaught TypeError: enableDebugMode may be "
"called at most once."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{});
}
// Requests where reject reason is specified and k-anonymity enforcement is
// active.
kanon_mode_ = mojom::KAnonymityBidMode::kEnforce;
{
PrivateAggregationRequests expected_non_kanon_pa_requests;
expected_non_kanon_pa_requests.push_back(
mojom::PrivateAggregationRequest::New(
mojom::AggregatableReportContribution::NewForEventContribution(
mojom::AggregatableReportForEventContribution::New(
/*bucket=*/mojom::ForEventSignalBucket::NewSignalBucket(
mojom::SignalBucket::New(
/*baseValue=*/mojom::BaseValue::kWinningBid,
/*scale=*/1.0,
/*offset=*/nullptr)),
/*value=*/
mojom::ForEventSignalValue::NewSignalValue(
mojom::SignalValue::New(
/*baseValue=*/mojom::BaseValue::kBidRejectReason,
/*scale=*/1.0,
/*offset=*/0)),
/*event_type=*/"reserved.loss")),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New()));
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
if (interestGroup.ads.length > 0) {
privateAggregation.sendHistogramReport(
{bucket: 18446744073709551616n, value: 1});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 18446744073709551616n, value: 2});
privateAggregation.reportContributionForEvent("reserved.loss", {
bucket: {baseValue: "highest-scoring-other-bid", offset: 0n},
value: 1,
});
privateAggregation.reportContributionForEvent("reserved.loss", {
bucket: {baseValue: "winning-bid"},
value: {baseValue: "bid-reject-reason"},
});
}
)"),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/ generateBid() bid render URL "
"'https://response.test/' isn't one of the registered creative URLs."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_update_priority_signals_overrides=*/{},
/*expected_pa_requests=*/{},
/*expected_non_kanon_pa_requests=*/
std::move(expected_non_kanon_pa_requests));
}
}
TEST_F(BidderWorkletPrivateAggregationEnabledTest, ReportWin) {
auction_worklet::mojom::PrivateAggregationRequest kExpectedRequest1(
mojom::AggregatableReportContribution::NewHistogramContribution(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/123,
/*value=*/45)),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
auction_worklet::mojom::PrivateAggregationRequest kExpectedRequest2(
mojom::AggregatableReportContribution::NewHistogramContribution(
blink::mojom::AggregatableReportHistogramContribution::New(
/*bucket=*/absl::MakeInt128(/*high=*/1, /*low=*/0),
/*value=*/1)),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
mojom::PrivateAggregationRequest kExpectedForEventRequest1(
mojom::AggregatableReportContribution::NewForEventContribution(
mojom::AggregatableReportForEventContribution::New(
/*bucket=*/mojom::ForEventSignalBucket::NewIdBucket(234),
/*value=*/mojom::ForEventSignalValue::NewIntValue(56),
/*event_type=*/"reserved.win")),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
mojom::PrivateAggregationRequest kExpectedForEventRequest2(
mojom::AggregatableReportContribution::NewForEventContribution(
mojom::AggregatableReportForEventContribution::New(
/*bucket=*/mojom::ForEventSignalBucket::NewIdBucket(
absl::MakeInt128(/*high=*/1,
/*low=*/0)),
/*value=*/mojom::ForEventSignalValue::NewIntValue(2),
/*event_type=*/"reserved.win")),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New());
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest1.Clone());
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, std::move(expected_pa_requests),
/*expected_errors=*/{});
}
// Set the private-aggregation permissions policy to disallowed.
{
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/false,
/*shared_storage_allowed=*/true);
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, /*expected_pa_requests=*/{},
/*expected_errors=*/
{"https://url.test/:12 Uncaught TypeError: The \"private-aggregation\" "
"Permissions Policy denied the method on privateAggregation."});
permissions_policy_state_ =
mojom::AuctionWorkletPermissionsPolicyState::New(
/*private_aggregation_allowed=*/true,
/*shared_storage_allowed=*/true);
}
// Large bucket
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest2.Clone());
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.sendHistogramReport({bucket: 18446744073709551616n,
value: 1});
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, std::move(expected_pa_requests),
/*expected_errors=*/{});
}
// Multiple requests
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest1.Clone());
expected_pa_requests.push_back(kExpectedRequest2.Clone());
expected_pa_requests.push_back(kExpectedForEventRequest1.Clone());
expected_pa_requests.push_back(kExpectedForEventRequest2.Clone());
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.sendHistogramReport({bucket: 18446744073709551616n,
value: 1});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 234n, value: 56});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 18446744073709551616n, value: 2});
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, std::move(expected_pa_requests),
/*expected_errors=*/{});
}
// An unrelated exception after sendHistogramReport shouldn't block the report
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest1.Clone());
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
error;
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, std::move(expected_pa_requests),
/*expected_errors=*/
{"https://url.test/:13 Uncaught ReferenceError: error is not "
"defined."});
}
// Debug mode enabled with debug key
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
kExpectedRequest1.contribution->Clone(),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, blink::mojom::DebugKey::New(1234u))));
expected_pa_requests.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
kExpectedForEventRequest1.contribution->Clone(),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, blink::mojom::DebugKey::New(1234u))));
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.enableDebugMode({debug_key: 1234n});
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 234n, value: 56});
)",
/*expected_report_url=*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, std::move(expected_pa_requests),
/*expected_errors=*/{});
}
// Debug mode enabled without debug key, but with multiple requests
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
kExpectedRequest1.contribution->Clone(),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, /*debug_key=*/nullptr)));
expected_pa_requests.push_back(
auction_worklet::mojom::PrivateAggregationRequest::New(
kExpectedRequest2.contribution->Clone(),
blink::mojom::AggregationServiceMode::kDefault,
blink::mojom::DebugModeDetails::New(
/*is_enabled=*/true, /*debug_key=*/nullptr)));
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.enableDebugMode();
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.sendHistogramReport(
{bucket: 18446744073709551616n, value: 1});
)",
/*expected_report_url=*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, std::move(expected_pa_requests),
/*expected_errors=*/{});
}
// For-event report and histogram report are reported.
{
PrivateAggregationRequests expected_pa_requests;
expected_pa_requests.push_back(kExpectedRequest1.Clone());
expected_pa_requests.push_back(kExpectedForEventRequest1.Clone());
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
privateAggregation.reportContributionForEvent(
"reserved.win", {bucket: 234n, value: 56});
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, std::move(expected_pa_requests),
/*expected_errors=*/{});
}
}
class BidderWorkletPrivateAggregationDisabledTest : public BidderWorkletTest {
public:
BidderWorkletPrivateAggregationDisabledTest() {
scoped_feature_list_.InitAndDisableFeature(
blink::features::kPrivateAggregationApi);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(BidderWorkletPrivateAggregationDisabledTest, GenerateBid) {
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: "ad", bid:1, render:"https://response.test/" })",
/*extra_code=*/R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
)"),
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/:6 Uncaught ReferenceError: privateAggregation is not "
"defined."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/absl::nullopt,
/*expected_pa_requests=*/{});
}
TEST_F(BidderWorkletPrivateAggregationDisabledTest, ReportWin) {
RunReportWinWithFunctionBodyExpectingResult(
R"(
privateAggregation.sendHistogramReport({bucket: 123n, value: 45});
)",
/*expected_report_url =*/absl::nullopt,
/*expected_ad_beacon_map=*/{}, /*expected_pa_requests=*/{},
/*expected_errors=*/
{"https://url.test/:12 Uncaught ReferenceError: privateAggregation is "
"not defined."});
}
TEST_F(BidderWorkletTest, KAnonSimulate) {
const char kSideEffectScript[] = R"(
setPriority(interestGroup.ads.length + 10);
)";
kanon_mode_ = auction_worklet::mojom::KAnonymityBidMode::kSimulate;
// Nothing returned regardless of filtering.
RunGenerateBidWithReturnValueExpectingResult(
"",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
EXPECT_FALSE(kanon_bid_);
// Sole bid is unauthorized. The non-enforced bid is there, kanon-bid isn't.
// Since this is simulation mode, set_priority and errors should come from the
// unrestricted run.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:1, render:"https://response.test/"})",
kSideEffectScript),
mojom::BidderWorkletBid::New(
R"(["ad"])", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/11);
ASSERT_FALSE(kanon_bid_);
// Now authorize it.
kanon_keys_.emplace(
auction_worklet::mojom::KAnonKey::New(blink::KAnonKeyForAdBid(
url::Origin::Create(interest_group_bidding_url_),
interest_group_bidding_url_, GURL("https://response.test/"))),
true);
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:1, render:"https://response.test/"})",
kSideEffectScript),
mojom::BidderWorkletBid::New(
R"(["ad"])", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/11);
ASSERT_TRUE(kanon_bid_);
EXPECT_TRUE(kanon_bid_->is_same_as_non_enforced());
// Add a second ad, not authorized yet, with script that it will try it
// if it's in the ad vector.
interest_group_ads_.emplace_back(GURL("https://response2.test/"),
/*metadata=*/absl::nullopt);
// Non-enforced bid will be 2. Since this is simulated mode, other things are
// from the same run, so expected_set_priority is 12.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:interestGroup.ads.length,
render:interestGroup.ads[interestGroup.ads.length - 1].renderUrl})",
kSideEffectScript),
mojom::BidderWorkletBid::New(
R"(["ad"])", 2, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response2.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/12);
// k-anon-enforced bid will be 1.
ASSERT_TRUE(kanon_bid_);
ASSERT_FALSE(kanon_bid_->is_same_as_non_enforced());
EXPECT_EQ(kanon_bid_->get_bid()->ad, R"(["ad"])");
EXPECT_EQ(kanon_bid_->get_bid()->bid, 1);
EXPECT_EQ(kanon_bid_->get_bid()->ad_descriptor.url,
GURL("https://response.test/"));
// Authorize it.
kanon_keys_.emplace(
auction_worklet::mojom::KAnonKey::New(blink::KAnonKeyForAdBid(
url::Origin::Create(interest_group_bidding_url_),
interest_group_bidding_url_, GURL("https://response2.test/"))),
true);
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:interestGroup.ads.length,
render:interestGroup.ads[interestGroup.ads.length - 1].renderUrl})",
kSideEffectScript),
mojom::BidderWorkletBid::New(
R"(["ad"])", 2, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response2.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/12);
ASSERT_TRUE(kanon_bid_);
EXPECT_TRUE(kanon_bid_->is_same_as_non_enforced());
}
TEST_F(BidderWorkletTest, KAnonEnforce) {
kanon_mode_ = auction_worklet::mojom::KAnonymityBidMode::kEnforce;
const char kSideEffectScript[] = R"(
setPriority(interestGroup.ads.length + 10);
)";
// Nothing returned regardless of filtering.
RunGenerateBidWithReturnValueExpectingResult(
"",
/*expected_bid=*/mojom::BidderWorkletBidPtr());
EXPECT_FALSE(kanon_bid_);
// Sole bid is unauthorized. The non-enforced bid is there, kanon-bid isn't.
// Since this is enforcement mode, set_priority and errors should come from
// the restricted run.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:1, render:"https://response.test/"})",
kSideEffectScript),
/*expected_bid=*/
mojom::BidderWorkletBid::New(
R"(["ad"])", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/
{"https://url.test/ generateBid() bid render URL 'https://response.test/'"
" isn't one of the registered creative URLs."},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/10);
ASSERT_FALSE(kanon_bid_);
// Now authorize it.
kanon_keys_.emplace(
auction_worklet::mojom::KAnonKey::New(blink::KAnonKeyForAdBid(
url::Origin::Create(interest_group_bidding_url_),
interest_group_bidding_url_, GURL("https://response.test/"))),
true);
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:1, render:"https://response.test/"})",
kSideEffectScript),
mojom::BidderWorkletBid::New(
R"(["ad"])", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/11);
ASSERT_TRUE(kanon_bid_);
EXPECT_TRUE(kanon_bid_->is_same_as_non_enforced());
// Add a second ad, not authorized yet, with script that it will try it
// if it's in the ad vector.
interest_group_ads_.emplace_back(GURL("https://response2.test/"),
/*metadata=*/absl::nullopt);
// Non-enforced bid will be 2. Since this is enforced mode, other things are
// from the restricted run, so expected_set_priority is 11.
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:interestGroup.ads.length,
render:interestGroup.ads[interestGroup.ads.length - 1].renderUrl})",
kSideEffectScript),
mojom::BidderWorkletBid::New(
R"(["ad"])", 2, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response2.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/11);
// k-anon-enforced bid will be 1.
ASSERT_TRUE(kanon_bid_);
ASSERT_FALSE(kanon_bid_->is_same_as_non_enforced());
EXPECT_EQ(kanon_bid_->get_bid()->ad, R"(["ad"])");
EXPECT_EQ(kanon_bid_->get_bid()->bid, 1);
EXPECT_EQ(kanon_bid_->get_bid()->ad_descriptor.url,
GURL("https://response.test/"));
// Authorize it.
kanon_keys_.emplace(
auction_worklet::mojom::KAnonKey::New(blink::KAnonKeyForAdBid(
url::Origin::Create(interest_group_bidding_url_),
interest_group_bidding_url_, GURL("https://response2.test/"))),
true);
RunGenerateBidWithJavascriptExpectingResult(
CreateGenerateBidScript(
R"({ad: ["ad"], bid:interestGroup.ads.length,
render:interestGroup.ads[interestGroup.ads.length - 1].renderUrl})",
kSideEffectScript),
mojom::BidderWorkletBid::New(
R"(["ad"])", 2, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response2.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()),
/*expected_data_version=*/absl::nullopt,
/*expected_errors=*/{},
/*expected_debug_loss_report_url=*/absl::nullopt,
/*expected_debug_win_report_url=*/absl::nullopt,
/*expected_set_priority=*/12);
ASSERT_TRUE(kanon_bid_);
EXPECT_TRUE(kanon_bid_->is_same_as_non_enforced());
}
// Test for context re-use for k-anon rerun.
TEST_F(BidderWorkletTest, KAnonRerun) {
const char kScript[] = R"(
if (!('count' in globalThis))
globalThis.count = 0;
function generateBid(interestGroup) {
++count;
return {ad: ["ad"], bid:count,
render:interestGroup.ads[interestGroup.ads.length - 1].renderUrl};
}
)";
kanon_mode_ = auction_worklet::mojom::KAnonymityBidMode::kEnforce;
interest_group_ads_.emplace_back(GURL("https://response2.test/"),
/*metadata=*/absl::nullopt);
kanon_keys_.emplace(
auction_worklet::mojom::KAnonKey::New(blink::KAnonKeyForAdBid(
url::Origin::Create(interest_group_bidding_url_),
interest_group_bidding_url_, GURL("https://response.test/"))),
true);
for (auto execution_mode :
{blink::mojom::InterestGroup::ExecutionMode::kCompatibilityMode,
blink::mojom::InterestGroup::ExecutionMode::kGroupedByOriginMode}) {
execution_mode_ = execution_mode;
SCOPED_TRACE(execution_mode_);
RunGenerateBidWithJavascriptExpectingResult(
kScript, mojom::BidderWorkletBid::New(
R"(["ad"])", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response2.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
ASSERT_TRUE(kanon_bid_);
ASSERT_TRUE(kanon_bid_->is_bid());
EXPECT_EQ(R"(["ad"])", kanon_bid_->get_bid()->ad);
EXPECT_EQ(2, kanon_bid_->get_bid()->bid);
EXPECT_EQ(GURL("https://response.test/"),
kanon_bid_->get_bid()->ad_descriptor.url);
}
}
TEST_F(BidderWorkletTest, IsKAnonURL) {
const GURL kUrl1("https://example.com/1");
const GURL kUrl2("https://example.org/2");
const GURL kUrl3("https://example.gov/3");
const GURL kUrl4("https://example.gov/4");
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
url::Origin owner = url::Origin::Create(interest_group_bidding_url_);
const std::string kUrl1KAnonKey =
blink::KAnonKeyForAdBid(owner, interest_group_bidding_url_, kUrl1);
const std::string kUrl2KAnonKey =
blink::KAnonKeyForAdBid(owner, interest_group_bidding_url_, kUrl2);
const std::string kUrl3KAnonKey =
blink::KAnonKeyForAdBid(owner, interest_group_bidding_url_, kUrl3);
const std::string kUrl4KAnonKey =
blink::KAnonKeyForAdBid(owner, interest_group_bidding_url_, kUrl4);
params->kanon_keys.emplace(
auction_worklet::mojom::KAnonKey::New(kUrl1KAnonKey), true);
params->kanon_keys.emplace(
auction_worklet::mojom::KAnonKey::New(kUrl2KAnonKey), true);
params->kanon_keys.emplace(
auction_worklet::mojom::KAnonKey::New(kUrl3KAnonKey), false);
EXPECT_TRUE(BidderWorklet::IsKAnon(params.get(), kUrl1KAnonKey));
EXPECT_TRUE(BidderWorklet::IsKAnon(params.get(), kUrl2KAnonKey));
EXPECT_FALSE(BidderWorklet::IsKAnon(params.get(), kUrl3KAnonKey));
EXPECT_FALSE(BidderWorklet::IsKAnon(params.get(), kUrl4KAnonKey));
}
TEST_F(BidderWorkletTest, IsKAnonResult) {
const GURL kUrl1("https://example.com/1");
const GURL kUrl2("https://example.org/2");
const GURL kUrl3("https://example.gov/3");
const GURL kUrl4("https://example.gov/4");
mojom::BidderWorkletNonSharedParamsPtr params =
mojom::BidderWorkletNonSharedParams::New();
url::Origin owner = url::Origin::Create(interest_group_bidding_url_);
params->kanon_keys.emplace(
auction_worklet::mojom::KAnonKey::New(
blink::KAnonKeyForAdBid(owner, interest_group_bidding_url_, kUrl1)),
true);
params->kanon_keys.emplace(auction_worklet::mojom::KAnonKey::New(
blink::KAnonKeyForAdComponentBid(kUrl2)),
true);
params->kanon_keys.emplace(auction_worklet::mojom::KAnonKey::New(
blink::KAnonKeyForAdComponentBid(kUrl3)),
false);
mojom::BidderWorkletBidPtr bid = mojom::BidderWorkletBid::New();
// k-anon ad URL.
bid->ad_descriptor.url = kUrl1;
EXPECT_TRUE(
BidderWorklet::IsKAnon(params.get(), interest_group_bidding_url_, bid));
// k-anon ad URL and component.
bid->ad_component_descriptors.emplace();
bid->ad_component_descriptors->emplace_back(kUrl2);
EXPECT_TRUE(
BidderWorklet::IsKAnon(params.get(), interest_group_bidding_url_, bid));
// Non k-anon ad URL, k-anon component.
bid->ad_descriptor.url = kUrl4;
EXPECT_FALSE(
BidderWorklet::IsKAnon(params.get(), interest_group_bidding_url_, bid));
// k-anon ad URL, one of the components non-k-anon.
bid->ad_descriptor.url = kUrl1;
bid->ad_component_descriptors->emplace_back(kUrl3);
EXPECT_FALSE(
BidderWorklet::IsKAnon(params.get(), interest_group_bidding_url_, bid));
}
// Test of handling of FinalizeGenerateBid that comes in after the trusted
// signals.
TEST_F(BidderWorkletTest, AsyncFinalizeGenerateBid) {
interest_group_trusted_bidding_signals_url_ =
GURL("https://url.test/trustedsignals");
interest_group_trusted_bidding_signals_keys_ = {"1"};
const char kSerializeParams[] =
R"({ad: [auctionSignals, trustedBiddingSignals,
perBuyerSignals], bid:1,
render:"https://response.test/"})";
// Add script, but not trusted signals yet.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(kSerializeParams));
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
BeginGenerateBid(bidder_worklet.get(),
bid_finalizer.BindNewEndpointAndPassReceiver());
task_environment_.RunUntilIdle();
EXPECT_FALSE(bid_);
// Add trusted signals, too.
AddBidderJsonResponse(&url_loader_factory_,
GURL("https://url.test/"
"trustedsignals?hostname=top.window.test&keys=1&"
"interestGroupNames=Fred"),
R"({"keys": {"1":123}})");
// Not enough yet.
task_environment_.RunUntilIdle();
EXPECT_FALSE(bid_);
// Now feed in the rest of the arguments.
bid_finalizer->FinishGenerateBid(
auction_signals_, per_buyer_signals_, per_buyer_timeout_,
per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(R"([["auction_signals"],{"1":123},["per_buyer_signals"]])",
bid_->ad);
EXPECT_EQ(1, bid_->bid);
EXPECT_THAT(bid_errors_, testing::ElementsAre());
}
// Test of handling of FinalizeGenerateBid that comes in before the trusted
// signals.
TEST_F(BidderWorkletTest, AsyncFinalizeGenerateBid2) {
interest_group_trusted_bidding_signals_url_ =
GURL("https://url.test/trustedsignals");
interest_group_trusted_bidding_signals_keys_ = {"1"};
const char kSerializeParams[] =
R"({ad: [auctionSignals, trustedBiddingSignals,
perBuyerSignals], bid:1,
render:"https://response.test/"})";
// Add script, but not trusted signals yet.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(kSerializeParams));
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
BeginGenerateBid(bidder_worklet.get(),
bid_finalizer.BindNewEndpointAndPassReceiver());
task_environment_.RunUntilIdle();
EXPECT_FALSE(bid_);
// Feed in the rest of the arguments.
bid_finalizer->FinishGenerateBid(
auction_signals_, per_buyer_signals_, per_buyer_timeout_,
per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
task_environment_.RunUntilIdle();
EXPECT_FALSE(bid_);
// Add trusted signals, too.
AddBidderJsonResponse(&url_loader_factory_,
GURL("https://url.test/"
"trustedsignals?hostname=top.window.test&keys=1&"
"interestGroupNames=Fred"),
R"({"keys": {"1":123}})");
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(R"([["auction_signals"],{"1":123},["per_buyer_signals"]])",
bid_->ad);
EXPECT_EQ(1, bid_->bid);
EXPECT_THAT(bid_errors_, testing::ElementsAre());
}
class GenerateBidLatenciesTest : public BidderWorkletTest {
public:
// We use MockTime to control the reported latencies.
GenerateBidLatenciesTest()
: BidderWorkletTest(TaskEnvironment::TimeSource::MOCK_TIME) {}
};
TEST_F(GenerateBidLatenciesTest, GenerateBidLatenciesAreReturned) {
interest_group_trusted_bidding_signals_url_ =
GURL("https://url.test/trustedsignals");
interest_group_trusted_bidding_signals_keys_ = {"1"};
const char kSerializeParams[] =
R"({ad: [auctionSignals, trustedBiddingSignals,
perBuyerSignals], bid:1,
render:"https://response.test/"})";
// Add script, but not trusted signals yet.
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
CreateGenerateBidScript(kSerializeParams));
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
BeginGenerateBid(bidder_worklet.get(),
bid_finalizer.BindNewEndpointAndPassReceiver());
task_environment_.RunUntilIdle();
EXPECT_FALSE(bid_);
task_environment_.FastForwardBy(base::Milliseconds(250));
// Add trusted signals, too.
AddBidderJsonResponse(&url_loader_factory_,
GURL("https://url.test/"
"trustedsignals?hostname=top.window.test&keys=1&"
"interestGroupNames=Fred"),
R"({"keys": {"1":123}})");
// Not enough yet.
task_environment_.RunUntilIdle();
EXPECT_FALSE(bid_);
task_environment_.FastForwardBy(base::Milliseconds(250));
// Now feed in the rest of the arguments.
bid_finalizer->FinishGenerateBid(
auction_signals_, per_buyer_signals_, per_buyer_timeout_,
per_buyer_currency_,
/*direct_from_seller_per_buyer_signals=*/absl::nullopt,
/*direct_from_seller_auction_signals=*/absl::nullopt);
load_script_run_loop_ = std::make_unique<base::RunLoop>();
load_script_run_loop_->Run();
ASSERT_TRUE(bid_);
EXPECT_EQ(R"([["auction_signals"],{"1":123},["per_buyer_signals"]])",
bid_->ad);
EXPECT_EQ(1, bid_->bid);
EXPECT_THAT(bid_errors_, testing::ElementsAre());
ASSERT_TRUE(generate_bid_dependency_latencies_);
// Any zero latency is replaced with nullopt.
EXPECT_FALSE(generate_bid_dependency_latencies_->code_ready_latency);
EXPECT_EQ(base::Milliseconds(500),
*generate_bid_dependency_latencies_->config_promises_latency);
EXPECT_FALSE(
generate_bid_dependency_latencies_->direct_from_seller_signals_latency);
EXPECT_EQ(
base::Milliseconds(250),
*generate_bid_dependency_latencies_->trusted_bidding_signals_latency);
}
// The sequence when GenerateBidClient gets destroyed w/o getting to
// FinalizeGenerateBid() needs to do some extra cleaning up, so exercise it.
TEST_F(BidderWorkletTest, CloseGenerateBidClientBeforeFinalize) {
mojo::Remote<mojom::BidderWorklet> bidder_worklet = CreateWorklet();
mojo::AssociatedRemote<auction_worklet::mojom::GenerateBidFinalizer>
bid_finalizer;
mojo::PendingAssociatedRemote<mojom::GenerateBidClient> generate_bid_client;
auto generate_bid_client_impl =
std::make_unique<GenerateBidClientWithCallbacks>(
GenerateBidClientWithCallbacks::GenerateBidNeverInvokedCallback());
mojo::AssociatedReceiver<mojom::GenerateBidClient>
generate_bid_client_receiver(
generate_bid_client_impl.get(),
generate_bid_client.InitWithNewEndpointAndPassReceiver());
BeginGenerateBid(bidder_worklet.get(),
bid_finalizer.BindNewEndpointAndPassReceiver(),
std::move(generate_bid_client));
task_environment_.RunUntilIdle();
// Drop this end of generate_bid_client pipe w/o getting to
// FinalizeGenerateBid.
generate_bid_client_receiver.reset();
task_environment_.RunUntilIdle();
// The finalizer pipe must have been closed, too.
EXPECT_FALSE(bid_finalizer.is_connected());
}
TEST_F(BidderWorkletTest, GenerateBidRenderUrlWithSize) {
// The 'render' field corresponds to an object that contains the url string.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid: 1, render: {url: "https://response.test/"}})",
/*expected_bid=*/mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// The 'render' field corresponds to an object that contains the url string
// and size with units.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "100sw",
height: "50px"
}
})",
/*expected_bid=*/mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(
GURL("https://response.test/"),
blink::AdSize(100, blink::AdSize::LengthUnit::kScreenWidth, 50,
blink::AdSize::LengthUnit::kPixels)),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// The 'render' field corresponds to an object that contains the url string
// and size without units.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "100",
height: "50"
}
})",
/*expected_bid=*/mojom::BidderWorkletBid::New(
"\"ad\"", 1, /*bid_currency=*/absl::nullopt,
/*ad_cost=*/absl::nullopt,
blink::AdDescriptor(
GURL("https://response.test/"),
blink::AdSize(100, blink::AdSize::LengthUnit::kPixels, 50,
blink::AdSize::LengthUnit::kPixels)),
/*ad_component_descriptors=*/absl::nullopt,
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// The 'render' field corresponds to an object with an invalid field.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid: 1, render: {foo: "https://response.test/"}})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has incorrect structure."});
// The 'render' field corresponds to an object with an invalid field.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "100sw",
height: "50px",
foo: 42
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has incorrect structure."});
// The 'render' field corresponds to an empty object.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid: 1, render: {}})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has incorrect structure."});
// The 'render' field corresponds to an object without url string field.
RunGenerateBidWithReturnValueExpectingResult(
R"({ad: "ad", bid: 1, render: {width: "100sw", height: "50px"}})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has incorrect structure."});
// Size is not of string type.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: 100,
height: 50
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has incorrect structure."});
// Only width is specified.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "100sw"
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has incorrect structure."});
// Only height is specified.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
height: "100sw"
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has incorrect structure."});
// Size has invalid units.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "100in",
height: "100px"
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has invalid size for render ad."});
// Size doesn't have numbers.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "sw",
height: "px"
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has invalid size for render ad."});
// Size is empty.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "",
height: ""
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has invalid size for render ad."});
// Size has zero value.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "0px",
height: "10px"
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has invalid size for render ad."});
// Size has negative value.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: "ad",
bid: 1,
render: {
url: "https://response.test/",
width: "-1px",
height: "10px"
}
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid has invalid size for render ad."});
}
TEST_F(BidderWorkletTest, GenerateBidAdComponentsWithSize) {
// The 'adComponents' field corresponds to an array of object that has the url
// string.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{url: "https://ad_component.test/"}
]
})",
/*expected_bid=*/mojom::BidderWorkletBid::New(
"0", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
std::vector<blink::AdDescriptor>{
blink::AdDescriptor(GURL("https://ad_component.test/"))},
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// The 'adComponents' field corresponds to an array of object that has the url
// string and size with units.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "100sw",
height: "50px"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBid::New(
"0", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
std::vector<blink::AdDescriptor>{blink::AdDescriptor(
GURL("https://ad_component.test/"),
blink::AdSize(100, blink::AdSize::LengthUnit::kScreenWidth, 50,
blink::AdSize::LengthUnit::kPixels))},
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// The 'adComponents' field corresponds to an array of object that has the url
// string and size without units.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "100",
height: "50"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBid::New(
"0", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
std::vector<blink::AdDescriptor>{blink::AdDescriptor(
GURL("https://ad_component.test/"),
blink::AdSize(100, blink::AdSize::LengthUnit::kPixels, 50,
blink::AdSize::LengthUnit::kPixels))},
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// The 'adComponents' field corresponds to an array of objects.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "100sw",
height: "50px"
},
{
url: "https://ad_component.test/"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBid::New(
"0", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
std::vector<blink::AdDescriptor>{
blink::AdDescriptor(
GURL("https://ad_component.test/"),
blink::AdSize(100, blink::AdSize::LengthUnit::kScreenWidth,
50, blink::AdSize::LengthUnit::kPixels)),
blink::AdDescriptor(GURL("https://ad_component.test/"))},
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// The 'adComponents' field corresponds to an array of url string and objects.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "100sw",
height: "50px"
},
{
url: "https://ad_component.test/"
},
"https://ad_component.test/"
]
})",
/*expected_bid=*/mojom::BidderWorkletBid::New(
"0", 1, /*bid_currency=*/absl::nullopt, /*ad_cost=*/absl::nullopt,
blink::AdDescriptor(GURL("https://response.test/")),
std::vector<blink::AdDescriptor>{
blink::AdDescriptor(
GURL("https://ad_component.test/"),
blink::AdSize(100, blink::AdSize::LengthUnit::kScreenWidth,
50, blink::AdSize::LengthUnit::kPixels)),
blink::AdDescriptor(GURL("https://ad_component.test/")),
blink::AdDescriptor(GURL("https://ad_component.test/"))},
/*modeling_signals=*/absl::nullopt, base::TimeDelta()));
// The 'adComponents' field corresponds to an array of an object that has an
// invalid field.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
foo: "https://ad_component.test/"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents value must be an "
"array of strings or objects that contain the url string field and "
"optional width and height fields."});
// The 'adComponents' field corresponds to an array of an object that has an
// invalid field.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render:"https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "100sw",
height: "50px",
foo: 42
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents value must be an "
"array of strings or objects that contain the url string field and "
"optional width and height fields."});
// The 'adComponents' field corresponds to an array of an empty object.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents value must be an "
"array of strings or objects that contain the url string field and "
"optional width and height fields."});
// The 'adComponents' field corresponds to an array of an object without url
// string field.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render:"https://response.test/",
adComponents: [
{
width: "100sw",
height: "50px"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents value must be an "
"array of strings or objects that contain the url string field and "
"optional width and height fields."});
// Size is not of string type.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
width: 100,
height: 50
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents value must be an "
"array of strings or objects that contain the url string field and "
"optional width and height fields."});
// Only width is specified.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "100sw"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents value must be an "
"array of strings or objects that contain the url string field and "
"optional width and height fields."});
// Only height is specified.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
height: "50px"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents value must be an "
"array of strings or objects that contain the url string field and "
"optional width and height fields."});
// Size has invalid units.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "100in",
height: "100px"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents have invalid size for "
"ad component."});
// Size doesn't have numbers.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents:
[
{
url: "https://ad_component.test/",
width: "sw",
height: "px"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents have invalid size for "
"ad component."});
// Size is empty.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "",
height: ""
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents have invalid size for "
"ad component."});
// Size has zero value.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "0px",
height: "1px"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents have invalid size for "
"ad component."});
// Size has negative value.
RunGenerateBidWithReturnValueExpectingResult(
R"({
ad: 0,
bid: 1,
render: "https://response.test/",
adComponents: [
{
url: "https://ad_component.test/",
width: "-1px",
height: "5px"
}
]
})",
/*expected_bid=*/mojom::BidderWorkletBidPtr(),
/*expected_data_version=*/absl::nullopt, /*expected_errors=*/
{"https://url.test/ generateBid() bid adComponents have invalid size for "
"ad component."});
}
} // namespace
} // namespace auction_worklet