| // Copyright 2021 The Chromium Authors. All rights reserved. |
| // 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 <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/memory/raw_ptr.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/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/self_owned_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/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/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 { |
| |
| // 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) { |
| %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) { |
| %s; |
| } |
| )"; |
| return CreateBasicGenerateBidScript() + |
| base::StringPrintf(kReportWinScript, function_body.c_str()); |
| } |
| |
| class BidderWorkletTest : public testing::Test { |
| public: |
| BidderWorkletTest() { 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"; |
| interest_group_user_bidding_signals_ = absl::nullopt; |
| |
| interest_group_ads_.clear(); |
| interest_group_ads_.emplace_back(blink::InterestGroup::Ad( |
| GURL("https://response.test/"), absl::nullopt /* metadata */)); |
| |
| interest_group_ad_components_.reset(); |
| interest_group_ad_components_.emplace(); |
| interest_group_ad_components_->emplace_back(blink::InterestGroup::Ad( |
| GURL("https://ad_component.test/"), absl::nullopt /* metadata */)); |
| |
| daily_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; |
| top_window_origin_ = url::Origin::Create(GURL("https://top.window.test/")); |
| 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_highest_scoring_other_bid_ = 0.5; |
| browser_signal_made_highest_scoring_other_bid_ = false; |
| data_version_.reset(); |
| } |
| |
| // 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) { |
| RunGenerateBidWithJavascriptExpectingResult( |
| CreateGenerateBidScript(raw_return_value), std::move(expected_bid), |
| expected_data_version, expected_errors, expected_debug_loss_report_url, |
| expected_debug_win_report_url); |
| } |
| |
| // 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) { |
| 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); |
| } |
| |
| // Loads and runs a generateBid() script, expecting the provided result. |
| 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) { |
| 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(expected_bid->render_url, bid_->render_url); |
| if (!expected_bid->ad_components) { |
| EXPECT_FALSE(bid_->ad_components); |
| } else { |
| EXPECT_THAT(*bid_->ad_components, |
| ::testing::ElementsAreArray(*expected_bid->ad_components)); |
| } |
| } |
| 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_errors, bid_errors_); |
| } |
| |
| // 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>(), |
| const std::vector<std::string>& expected_errors = |
| std::vector<std::string>()) { |
| RunReportWinWithJavascriptExpectingResult( |
| CreateReportWinScript(function_body), expected_report_url, |
| expected_ad_beacon_map, 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>(), |
| 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, |
| 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, |
| const std::vector<std::string>& expected_errors, |
| base::OnceClosure done_closure) { |
| bidder_worklet->ReportWin( |
| interest_group_name_, auction_signals_, per_buyer_signals_, |
| seller_signals_, browser_signal_render_url_, browser_signal_bid_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_made_highest_scoring_other_bid_, |
| browser_signal_seller_origin_, browser_signal_top_level_seller_origin_, |
| data_version_.value_or(0), data_version_.has_value(), |
| base::BindOnce( |
| [](const absl::optional<GURL>& expected_report_url, |
| const base::flat_map<std::string, GURL>& expected_ad_beacon_map, |
| 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, |
| 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); |
| std::move(done_closure).Run(); |
| }, |
| expected_report_url, expected_ad_beacon_map, 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>(), |
| 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, expected_errors, |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| // Creates a BidderWorkletNonSharedParams based on test fixture |
| // configuration. |
| mojom::BidderWorkletNonSharedParamsPtr CreateBidderWorkletNonSharedParams() { |
| return mojom::BidderWorkletNonSharedParams::New( |
| interest_group_name_, daily_update_url_, |
| interest_group_trusted_bidding_signals_keys_, |
| interest_group_user_bidding_signals_, interest_group_ads_, |
| interest_group_ad_components_); |
| } |
| |
| // 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) { |
| CHECK(!load_script_run_loop_); |
| |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory; |
| url_loader_factory_.Clone( |
| url_loader_factory.InitWithNewPipeAndPassReceiver()); |
| |
| auto bidder_worklet_impl = std::make_unique<BidderWorklet>( |
| v8_helper_, 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_); |
| 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; |
| } |
| |
| void GenerateBid(mojom::BidderWorklet* bidder_worklet) { |
| bidder_worklet->GenerateBid( |
| CreateBidderWorkletNonSharedParams(), auction_signals_, |
| per_buyer_signals_, per_buyer_timeout_, browser_signal_seller_origin_, |
| browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(), |
| auction_start_time_, |
| base::BindOnce(&BidderWorkletTest::GenerateBidCallback, |
| base::Unretained(this))); |
| bidder_worklet->SendPendingSignalsRequests(); |
| } |
| |
| // Calls GenerateBid(), expecting the callback never to be invoked. |
| void GenerateBidExpectingCallbackNotInvoked( |
| mojom::BidderWorklet* bidder_worklet) { |
| bidder_worklet->GenerateBid( |
| CreateBidderWorkletNonSharedParams(), auction_signals_, |
| per_buyer_signals_, per_buyer_timeout_, browser_signal_seller_origin_, |
| browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(), |
| auction_start_time_, |
| base::BindOnce([](mojom::BidderWorkletBidPtr 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, |
| const std::vector<std::string>& errors) { |
| ADD_FAILURE() << "Callback should not be invoked."; |
| })); |
| bidder_worklet->SendPendingSignalsRequests(); |
| } |
| |
| // Create a BidderWorklet and invokes GenerateBid(), 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, |
| uint32_t data_version, |
| bool has_data_version, |
| const absl::optional<GURL>& debug_loss_report_url, |
| const absl::optional<GURL>& debug_win_report_url, |
| const std::vector<std::string>& errors) { |
| absl::optional<uint32_t> maybe_data_version; |
| if (has_data_version) |
| maybe_data_version = data_version; |
| bid_ = std::move(bid); |
| data_version_ = maybe_data_version; |
| bid_debug_loss_report_url_ = debug_loss_report_url; |
| bid_debug_win_report_url_ = debug_win_report_url; |
| 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; |
| } |
| |
| base::test::TaskEnvironment task_environment_; |
| |
| // Values used to construct the BiddingInterestGroup passed to the |
| // BidderWorklet. |
| std::string interest_group_name_; |
| GURL interest_group_bidding_url_ = GURL("https://url.test/"); |
| 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_; |
| absl::optional<GURL> daily_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<base::TimeDelta> per_buyer_timeout_; |
| url::Origin top_window_origin_; |
| url::Origin browser_signal_seller_origin_; |
| absl::optional<url::Origin> browser_signal_top_level_seller_origin_; |
| |
| 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_; |
| double browser_signal_highest_scoring_other_bid_; |
| bool browser_signal_made_highest_scoring_other_bid_; |
| |
| // 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_; |
| absl::optional<GURL> bid_debug_loss_report_url_; |
| absl::optional<GURL> bid_debug_win_report_url_; |
| std::vector<std::string> bid_errors_; |
| |
| network::TestURLLoaderFactory url_loader_factory_; |
| scoped_refptr<AuctionV8Helper> v8_helper_; |
| |
| // 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(); |
| GenerateBidExpectingCallbackNotInvoked(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(); |
| GenerateBidExpectingCallbackNotInvoked(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(); |
| GenerateBidExpectingCallbackNotInvoked(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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: ["ad"], bid:1, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New( |
| R"(["ad"])", 1, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: [2.5,[]], bid:1, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New( |
| "[2.5,[]]", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: -5, bid:1, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New("-5", 1, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: [globalThis.not_defined], bid:1, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New("[null]", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: [function() {return 1;}], bid:1, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New("[null]", 1, GURL("https://response.test/"), |
| /*ad_components=*/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) { |
| // Missing value. |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: "ad", render:"https://response.test/"})", |
| /*expected_bid=*/mojom::BidderWorkletBidPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| {"https://url.test/ generateBid() bid has incorrect structure."}); |
| |
| // Valid positive bid values. |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: "ad", bid:1, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New("\"ad\"", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: "ad", bid:1.5, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New( |
| "\"ad\"", 1.5, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: "ad", bid:2, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New("\"ad\"", 2, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: "ad", bid:0.001, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New( |
| "\"ad\"", 0.001, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| // Bids <= 0. |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: ["ad"], bid:0, render:"https://response.test/"})", |
| mojom::BidderWorkletBidPtr() /* expected_bid */); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: ["ad"], bid:-10, render:"https://response.test/"})", |
| /*expected_bid=*/mojom::BidderWorkletBidPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| {"https://url.test/ generateBid() bid of -10.000000 is not a valid " |
| "bid."}); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: ["ad"], bid:-1.5, render:"https://response.test/"})", |
| /*expected_bid=*/mojom::BidderWorkletBidPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| {"https://url.test/ generateBid() bid of -1.500000 is not a valid bid."}); |
| |
| // 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() bid has incorrect structure."}); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: ["ad"], bid:[1], render:"https://response.test/"})", |
| /*expected_bid=*/mojom::BidderWorkletBidPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| {"https://url.test/ generateBid() bid has incorrect structure."}); |
| } |
| |
| TEST_F(BidderWorkletTest, GenerateBidReturnValueUrl) { |
| // Missing value. |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: ["ad"], 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| // 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: "ad", |
| bid:1, |
| render:"https://response.test/", |
| adComponents:null})", |
| mojom::BidderWorkletBid::New("\"ad\"", 1, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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."}); |
| |
| // 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, GURL("https://response.test/"), |
| /*ad_components=*/ |
| std::vector<GURL>{ |
| GURL("https://ad_component.test/") /* 1 */, |
| GURL("https://ad_component.test/") /* 2 */, |
| GURL("https://ad_component.test/") /* 3 */, |
| GURL("https://ad_component.test/") /* 4 */, |
| GURL("https://ad_component.test/") /* 5 */, |
| GURL("https://ad_component.test/") /* 6 */, |
| GURL("https://ad_component.test/") /* 7 */, |
| GURL("https://ad_component.test/") /* 8 */, |
| GURL("https://ad_component.test/") /* 9 */, |
| GURL("https://ad_component.test/") /* 10 */, |
| GURL("https://ad_component.test/") /* 11 */, |
| GURL("https://ad_component.test/") /* 12 */, |
| GURL("https://ad_component.test/") /* 13 */, |
| GURL("https://ad_component.test/") /* 14 */, |
| GURL("https://ad_component.test/") /* 15 */, |
| GURL("https://ad_component.test/") /* 16 */, |
| GURL("https://ad_component.test/") /* 17 */, |
| GURL("https://ad_component.test/") /* 18 */, |
| GURL("https://ad_component.test/") /* 19 */, |
| GURL("https://ad_component.test/") /* 20 */, |
| }, |
| 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, GenerateBidReturnValueInvalid) { |
| // No return value. |
| RunGenerateBidWithReturnValueExpectingResult( |
| "", /*expected_bid=*/mojom::BidderWorkletBidPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| {"https://url.test/ generateBid() bid not an object."}); |
| |
| // 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: bid has incorrect structure."}); |
| 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: bid has incorrect structure."}); |
| |
| // --------- |
| // 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."}); |
| |
| // 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: bid has incorrect structure."}); |
| 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: bid has incorrect structure."}); |
| 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: bid has incorrect structure."}); |
| } |
| |
| // 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/:5 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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| interest_group_name_ = R"("foo")"; |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New( |
| R"("\"foo\"")", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| interest_group_name_ = "[1]"; |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New( |
| R"("[1]")", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| } |
| |
| 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| interest_group_bidding_url_ = GURL("https://url.test/foo"); |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New( |
| R"("https://url.test/foo")", 1, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, |
| GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| } |
| |
| TEST_F(BidderWorkletTest, GenerateBidInterestGroupDailyUpdateUrl) { |
| const std::string kGenerateBidBody = |
| R"({ad: "dailyUpdateUrl" in interestGroup ? |
| interestGroup.dailyUpdateUrl : "missing", |
| bid:1, |
| render:"https://response.test/"})"; |
| |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New( |
| R"("missing")", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| daily_update_url_ = GURL("https://url.test/daily_update"); |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New(R"("https://url.test/daily_update")", 1, |
| GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| // Since there are no keys, this won't actually be requested, so no need to |
| // add a trusted bidding signals response. |
| interest_group_trusted_bidding_signals_url_ = |
| GURL("https://signals.test/foo.json"); |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New(R"("https://signals.test/foo.json")", 1, |
| GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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 strings should result in |
| // failures. |
| interest_group_user_bidding_signals_ = "foo"; |
| RunGenerateBidWithReturnValueExpectingResult(kGenerateBidBody, |
| mojom::BidderWorkletBidPtr()); |
| |
| interest_group_user_bidding_signals_ = R"("foo")"; |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New( |
| R"("foo")", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| interest_group_user_bidding_signals_ = "[1]"; |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New("[1]", 1, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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; |
| bidder_worklet->GenerateBid( |
| CreateBidderWorkletNonSharedParams(), |
| /*auction_signals_json=*/base::NumberToString(bid_value), |
| per_buyer_signals_, per_buyer_timeout_, browser_signal_seller_origin_, |
| browser_signal_top_level_seller_origin_, |
| CreateBiddingBrowserSignals(), auction_start_time_, |
| base::BindLambdaForTesting( |
| [&run_loop, &num_generate_bid_calls, bid_value]( |
| mojom::BidderWorkletBidPtr 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, |
| 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->render_url); |
| EXPECT_FALSE(has_data_version); |
| EXPECT_TRUE(errors.empty()); |
| ++num_generate_bid_calls; |
| if (num_generate_bid_calls == kNumGenerateBidCalls) |
| 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 (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) { |
| GenerateBidExpectingCallbackNotInvoked(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)); |
| bidder_worklet->GenerateBid( |
| std::move(interest_group_fields), auction_signals_, per_buyer_signals_, |
| per_buyer_timeout_, browser_signal_seller_origin_, |
| browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(), |
| auction_start_time_, |
| base::BindLambdaForTesting( |
| [&run_loop, &num_generate_bid_calls, i]( |
| mojom::BidderWorkletBidPtr 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, |
| 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->render_url); |
| 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(); |
| })); |
| } |
| // 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; |
| std::string json; |
| for (size_t i = 0; i < kNumGenerateBidCalls; ++i) { |
| if (i != 0) { |
| keys.append(","); |
| json.append(","); |
| } |
| keys.append(base::NumberToString(i)); |
| json.append(base::StringPrintf(R"("%zu":%zu)", i, i + 1)); |
| } |
| AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL(base::StringPrintf( |
| "https://signals.test/?hostname=top.window.test&keys=%s", |
| keys.c_str())), |
| base::StringPrintf("{%s}", json.c_str()), 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)); |
| bidder_worklet->GenerateBid( |
| std::move(interest_group_fields), auction_signals_, per_buyer_signals_, |
| per_buyer_timeout_, browser_signal_seller_origin_, |
| browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(), |
| auction_start_time_, |
| base::BindLambdaForTesting( |
| [&run_loop, &num_generate_bid_calls, i]( |
| mojom::BidderWorkletBidPtr 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, |
| 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->render_url); |
| 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(); |
| })); |
| } |
| // 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; |
| std::string json; |
| for (size_t i = 0; i < kNumGenerateBidCalls; ++i) { |
| if (i != 0) { |
| keys.append(","); |
| json.append(","); |
| } |
| keys.append(base::NumberToString(i)); |
| json.append(base::StringPrintf(R"("%zu":%zu)", i, i + 1)); |
| } |
| AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL(base::StringPrintf( |
| "https://signals.test/?hostname=top.window.test&keys=%s", |
| keys.c_str())), |
| base::StringPrintf("{%s}", json.c_str()), 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)); |
| bidder_worklet->GenerateBid( |
| std::move(interest_group_fields), auction_signals_, per_buyer_signals_, |
| per_buyer_timeout_, browser_signal_seller_origin_, |
| browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(), |
| auction_start_time_, |
| base::BindLambdaForTesting( |
| [&run_loop, &num_generate_bid_calls, i]( |
| mojom::BidderWorkletBidPtr 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, |
| const std::vector<std::string>& errors) { |
| EXPECT_EQ(base::NumberToString(i), bid->ad); |
| EXPECT_EQ(i + 1, bid->bid); |
| EXPECT_EQ(22u, data_version); |
| EXPECT_TRUE(has_data_version); |
| EXPECT_EQ(GURL("https://response.test/"), bid->render_url); |
| EXPECT_TRUE(errors.empty()); |
| ++num_generate_bid_calls; |
| if (num_generate_bid_calls == kNumGenerateBidCalls) |
| run_loop.Quit(); |
| })); |
| } |
| // 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; |
| std::string json; |
| for (size_t i = 0; i < kNumGenerateBidCalls; ++i) { |
| if (i != 0) { |
| keys.append(","); |
| json.append(","); |
| } |
| keys.append(base::NumberToString(i)); |
| json.append(base::StringPrintf(R"("%zu":%zu)", i, i + 1)); |
| } |
| AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL(base::StringPrintf( |
| "https://signals.test/?hostname=top.window.test&keys=%s", |
| keys.c_str())), |
| base::StringPrintf("{%s}", json.c_str()), 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) 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)); |
| bidder_worklet->GenerateBid( |
| std::move(interest_group_fields), auction_signals_, per_buyer_signals_, |
| per_buyer_timeout_, browser_signal_seller_origin_, |
| browser_signal_top_level_seller_origin_, CreateBiddingBrowserSignals(), |
| auction_start_time_, |
| base::BindLambdaForTesting( |
| [&run_loop, &num_generate_bid_calls, i]( |
| mojom::BidderWorkletBidPtr 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, |
| 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->render_url); |
| 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(); |
| })); |
| |
| // Send one request at a time. |
| 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. |
| for (size_t i = 0; i < kNumGenerateBidCalls; ++i) { |
| AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL(base::StringPrintf( |
| "https://signals.test/?hostname=top.window.test&keys=%zu", i)), |
| base::StringPrintf(R"({"%zu":%zu})", i, i + 1), i); |
| } |
| |
| // The worklets can now generate bids. |
| run_loop.Run(); |
| EXPECT_EQ(kNumGenerateBidCalls, num_generate_bid_calls); |
| } |
| |
| 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| auction_signals_ = "[1]"; |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New("[1]", 1, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| per_buyer_signals_ = "[1]"; |
| RunGenerateBidWithReturnValueExpectingResult( |
| kGenerateBidBody, |
| mojom::BidderWorkletBid::New("[1]", 1, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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(blink::InterestGroup::Ad( |
| GURL("https://response2.test/"), R"(["metadata"])" /* 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, GURL("https://response2.test/"), /*ad_components=*/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, GURL("https://response.test/"), |
| /*ad_components=*/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, GURL("https://response.test/"), |
| std::vector<GURL>{GURL("https://ad_component.test/")}, |
| base::TimeDelta())); |
| // Empty, but non-null, adComponents field. |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: 0, bid:1, render:"https://response.test/", adComponents:[]})", |
| mojom::BidderWorkletBid::New("0", 1, GURL("https://response.test/"), |
| std::vector<GURL>(), 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(blink::InterestGroup::Ad( |
| 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, GURL("https://response.test/"), |
| std::vector<GURL>{GURL("https://ad_component.test/"), |
| GURL("https://ad_component2.test/")}, |
| 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, GURL("https://response.test/"), |
| /*ad_components=*/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(); |
| GenerateBidExpectingCallbackNotInvoked(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(); |
| GenerateBidExpectingCallbackNotInvoked(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, |
| GURL("https://response.test/"), |
| /*ad_components=*/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( |
| interest_group_name_, /*auction_signals_json=*/"0", per_buyer_signals_, |
| seller_signals_, browser_signal_render_url_, browser_signal_bid_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_made_highest_scoring_other_bid_, |
| browser_signal_seller_origin_, browser_signal_top_level_seller_origin_, |
| data_version_.value_or(0), data_version_.has_value(), |
| base::BindLambdaForTesting( |
| [&run_loop](const absl::optional<GURL>& report_url, |
| const base::flat_map<std::string, GURL>& ad_beacon_map, |
| 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, 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. |
| GenerateBidExpectingCallbackNotInvoked(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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| } |
| } |
| |
| TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignals) { |
| const GURL kBaseSignalsUrl("https://signals.test/"); |
| const GURL kFullSignalsUrl( |
| "https://signals.test/?hostname=top.window.test&keys=key1,key2"); |
| |
| const char kJson[] = R"( |
| { |
| "key1": 1, |
| "key2": [2] |
| } |
| )"; |
| |
| // 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| |
| // 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| |
| // Request with TrustedBiddingSignals URL and null keys. No request should be |
| // made. |
| interest_group_trusted_bidding_signals_url_ = kBaseSignalsUrl; |
| interest_group_trusted_bidding_signals_keys_.reset(); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New("null", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| |
| // Request with TrustedBiddingSignals URL and empty keys. No request should be |
| // made. |
| 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta())); |
| |
| // 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta()), |
| /*expected_data_version=*/absl::nullopt, |
| {"Failed to load " |
| "https://signals.test/?hostname=top.window.test&keys=key1,key2 HTTP " |
| "status = 404 Not Found."}); |
| |
| // Request with valid TrustedBiddingSignals URL and non-empty keys. Request |
| // should be made. The request succeeds. |
| AddJsonResponse(&url_loader_factory_, kFullSignalsUrl, kJson); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: trustedBiddingSignals, bid:1, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New( |
| R"({"key1":1,"key2":[2]})", 1, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| } |
| |
| 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"); |
| AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL("https://signals.test/?hostname=top.window.test&keys=key1"), |
| R"({"key1":1})", 7u); |
| RunGenerateBidWithReturnValueExpectingResult( |
| R"({ad: "ad", bid:browserSignals.dataVersion, render:"https://response.test/"})", |
| mojom::BidderWorkletBid::New(R"("ad")", 7, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta()), |
| 7u); |
| } |
| |
| // Even though the script had set an intermediate result with setBid, the |
| // returned value should be used instead. |
| TEST_F(BidderWorkletTest, GenerateBidWithSetBid) { |
| RunGenerateBidWithJavascriptExpectingResult( |
| CreateGenerateBidScript( |
| /*raw_return_value=*/ |
| R"({ad: "returned", bid:2, render:"https://response.test/" })", |
| /*extra_code=*/R"( |
| setBid({ad: "ad", bid:1, render:"https://response.test/"}) |
| )"), |
| /*expected_bid=*/ |
| mojom::BidderWorkletBid::New( |
| "\"returned\"", 2, GURL("https://response.test/"), |
| /*ad_components=*/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 instead. |
| TEST_F(BidderWorkletTest, GenerateBidTimedOutWithSetBid) { |
| // The bidding script has an endless while loop. It will time out due to |
| // AuctionV8Helper's default script timeout (500 ms). |
| 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta()), |
| /*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. It will time out due to |
| // AuctionV8Helper's default script timeout (50 ms). |
| 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, |
| base::TimeDelta()), |
| /*expected_data_version=*/absl::nullopt, |
| {"https://url.test/ execution of `generateBid` timed out."}); |
| } |
| |
| 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=*/{}, |
| {"https://url.test/:10 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=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: sendReportTo must be passed a " |
| "valid HTTPS url."}); |
| |
| RunReportWinWithFunctionBodyExpectingResult( |
| R"(sendReportTo(""))", /*expected_report_url =*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| {"https://url.test/:10 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=*/{}, |
| {"https://url.test/:10 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, GURL("https://response.test/"), |
| /*ad_components=*/absl::nullopt, base::TimeDelta())); |
| |
| RunGenerateBidWithJavascriptExpectingResult( |
| CreateBasicGenerateBidScriptWithDebuggingReport( |
| R"(forDebuggingOnly.reportAdAuctionWin("https://win.url"))"), |
| mojom::BidderWorkletBid::New( |
| "[\"ad\"]", 1, GURL("https://response.test/"), |
| /*ad_components=*/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( |
| interest_group_name_, auction_signals_, per_buyer_signals_, |
| seller_signals_, browser_signal_render_url_, browser_signal_bid_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_made_highest_scoring_other_bid_, |
| browser_signal_seller_origin_, browser_signal_top_level_seller_origin_, |
| data_version_.value_or(0), data_version_.has_value(), |
| base::BindOnce([](const absl::optional<GURL>& report_url, |
| const base::flat_map<std::string, GURL>& ad_beacon_map, |
| 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( |
| interest_group_name_, |
| /*auction_signals_json=*/base::NumberToString(i), per_buyer_signals_, |
| seller_signals_, browser_signal_render_url_, browser_signal_bid_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_made_highest_scoring_other_bid_, |
| browser_signal_seller_origin_, |
| browser_signal_top_level_seller_origin_, data_version_.value_or(0), |
| data_version_.has_value(), |
| 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, |
| 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( |
| interest_group_name_, |
| /*auction_signals_json=*/base::NumberToString(i), per_buyer_signals_, |
| seller_signals_, browser_signal_render_url_, browser_signal_bid_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_made_highest_scoring_other_bid_, |
| browser_signal_seller_origin_, browser_signal_top_level_seller_origin_, |
| data_version_.value_or(0), data_version_.has_value(), |
| base::BindOnce( |
| [](const absl::optional<GURL>& report_url, |
| const base::flat_map<std::string, GURL>& ad_beacon_map, |
| 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=*/{}, |
| {"https://url.test/:10 Uncaught ReferenceError: Date is not defined."}); |
| } |
| |
| TEST_F(BidderWorkletTest, ReportWinInterestGroupName) { |
| interest_group_name_ = "https://interest.group.name.test/"; |
| RunReportWinWithFunctionBodyExpectingResult( |
| "sendReportTo(browserSignals.interestGroupName)", |
| GURL(interest_group_name_)); |
| } |
| |
| TEST_F(BidderWorkletTest, ReportWinDataVersion) { |
| data_version_ = 5u; |
| RunReportWinWithFunctionBodyExpectingResult( |
| "sendReportTo('https://dataVersion/'+browserSignals.dataVersion)", |
| GURL("https://dataVersion/5")); |
| } |
| |
| 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(), |
|