| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/services/auction_worklet/seller_worklet.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/containers/flat_map.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/test/bind.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/values_test_util.h" |
| #include "base/time/time.h" |
| #include "content/services/auction_worklet/auction_v8_helper.h" |
| #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h" |
| #include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h" |
| #include "content/services/auction_worklet/worklet_devtools_debug_test_util.h" |
| #include "content/services/auction_worklet/worklet_test_util.h" |
| #include "content/services/auction_worklet/worklet_v8_debug_test_util.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.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_currencies.h" |
| #include "third_party/blink/public/common/interest_group/auction_config.h" |
| #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h" |
| #include "url/gurl.h" |
| |
| using testing::HasSubstr; |
| using testing::StartsWith; |
| |
| namespace auction_worklet { |
| namespace { |
| |
| using PrivateAggregationRequests = |
| std::vector<mojom::PrivateAggregationRequestPtr>; |
| |
| // Very short time used by some tests that want to wait until just before a |
| // timer triggers. |
| constexpr base::TimeDelta kTinyTime = base::Microseconds(1); |
| |
| // Common trusted scoring signals response. |
| const char kTrustedScoringSignalsResponse[] = R"( |
| { |
| "renderUrls": {"https://render.url.test/": 4}, |
| "adComponentRenderUrls": { |
| "https://component1.test/": 1, |
| "https://component2.test/": 2 |
| } |
| } |
| )"; |
| |
| // Creates a seller script with scoreAd() returning the specified expression. |
| // Allows using scoreAd() arguments, arbitrary values, incorrect types, etc. |
| std::string CreateScoreAdScript(const std::string& raw_return_value, |
| const std::string& extra_code = "") { |
| constexpr char kSellAdScript[] = R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals, directFromSellerSignals) { |
| %s; |
| return %s; |
| } |
| )"; |
| return base::StringPrintf(kSellAdScript, extra_code.c_str(), |
| raw_return_value.c_str()); |
| } |
| |
| // Returns a working script, primarily for testing failure cases where it |
| // should not be run. |
| std::string CreateBasicSellAdScript() { |
| return CreateScoreAdScript("1"); |
| } |
| |
| // Creates a seller script with report_result() returning the specified |
| // expression. If |extra_code| is non-empty, it will be added as an additional |
| // line above the return value. Intended for sendReportTo() calls. In practice, |
| // these scripts will always include a scoreAd() method, but few tests check |
| // both methods. |
| std::string CreateReportToScript(const std::string& raw_return_value, |
| const std::string& extra_code) { |
| constexpr char kBasicSellerScript[] = R"( |
| function reportResult(auctionConfig, browserSignals, |
| directFromSellerSignals) { |
| %s; |
| return %s; |
| } |
| )"; |
| return CreateBasicSellAdScript() + |
| base::StringPrintf(kBasicSellerScript, extra_code.c_str(), |
| raw_return_value.c_str()); |
| } |
| |
| // A ScoreAdClient that takes a callback to call in OnScoreAdComplete(). |
| class TestScoreAdClient : public mojom::ScoreAdClient { |
| public: |
| using ScoreAdCompleteCallback = base::OnceCallback<void( |
| double score, |
| mojom::RejectReason reject_reason, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| component_auction_modified_bid_params, |
| absl::optional<double> bid_in_seller_currency, |
| absl::optional<uint32_t> scoring_signals_data_version, |
| const absl::optional<GURL>& debug_loss_report_url, |
| const absl::optional<GURL>& debug_win_report_url, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors)>; |
| |
| explicit TestScoreAdClient(ScoreAdCompleteCallback score_ad_complete_callback) |
| : score_ad_complete_callback_(std::move(score_ad_complete_callback)) {} |
| |
| ~TestScoreAdClient() override = default; |
| |
| // Helper that creates a TestScoreAdClient owned by a SelfOwnedReceiver. |
| static mojo::PendingRemote<mojom::ScoreAdClient> Create( |
| ScoreAdCompleteCallback score_ad_complete_callback) { |
| mojo::PendingRemote<mojom::ScoreAdClient> client_remote; |
| mojo::MakeSelfOwnedReceiver(std::make_unique<TestScoreAdClient>( |
| std::move(score_ad_complete_callback)), |
| client_remote.InitWithNewPipeAndPassReceiver()); |
| return client_remote; |
| } |
| |
| // mojom::ScoreAdClient implementation: |
| void OnScoreAdComplete(double score, |
| mojom::RejectReason reject_reason, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| component_auction_modified_bid_params, |
| absl::optional<double> bid_in_seller_currency, |
| absl::optional<uint32_t> scoring_signals_data_version, |
| const absl::optional<GURL>& debug_loss_report_url, |
| const absl::optional<GURL>& debug_win_report_url, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) override { |
| std::move(score_ad_complete_callback_) |
| .Run(score, reject_reason, |
| std::move(component_auction_modified_bid_params), |
| std::move(bid_in_seller_currency), |
| std::move(scoring_signals_data_version), debug_loss_report_url, |
| debug_win_report_url, std::move(pa_requests), errors); |
| } |
| |
| static ScoreAdCompleteCallback ScoreAdNeverInvokedCallback() { |
| return base::BindOnce( |
| [](double score, mojom::RejectReason reject_reason, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| component_auction_modified_bid_params, |
| absl::optional<double> bid_in_seller_currency, |
| absl::optional<uint32_t> scoring_signals_data_version, |
| const absl::optional<GURL>& debug_loss_report_url, |
| const absl::optional<GURL>& debug_win_report_url, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) { |
| ADD_FAILURE() << "Callback should not be invoked"; |
| }); |
| } |
| |
| private: |
| ScoreAdCompleteCallback score_ad_complete_callback_; |
| }; |
| |
| class SellerWorkletTest : public testing::Test { |
| public: |
| explicit SellerWorkletTest( |
| base::test::TaskEnvironment::TimeSource time_mode = |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) |
| : task_environment_(time_mode) { |
| SetDefaultParameters(); |
| } |
| |
| ~SellerWorkletTest() 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 |
| // 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(); |
| |
| // In all tests where the SellerWorklet receiver is closed before the |
| // remote, the disconnect reason should be consumed and validated. |
| EXPECT_FALSE(disconnect_reason_); |
| } |
| |
| // Sets default values for scoreAd() and report_result() arguments. No test |
| // actually depends on these being anything but valid, but this does allow |
| // tests to reset them to a consistent state. |
| void SetDefaultParameters() { |
| ad_metadata_ = "[1]"; |
| bid_ = 1; |
| bid_currency_ = absl::nullopt; |
| decision_logic_url_ = GURL("https://url.test/"); |
| trusted_scoring_signals_url_.reset(); |
| auction_ad_config_non_shared_params_ = |
| blink::AuctionConfig::NonSharedParams(); |
| |
| top_window_origin_ = url::Origin::Create(GURL("https://window.test/")); |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/true, |
| /*shared_storage_allowed=*/true); |
| experiment_group_id_ = absl::nullopt; |
| browser_signals_other_seller_.reset(); |
| component_expect_bid_currency_ = absl::nullopt; |
| browser_signal_interest_group_owner_ = |
| url::Origin::Create(GURL("https://interest.group.owner.test/")); |
| browser_signal_buyer_and_seller_reporting_id_ = absl::nullopt; |
| browser_signal_render_url_ = GURL("https://render.url.test/"); |
| browser_signal_ad_components_.clear(); |
| browser_signal_bidding_duration_msecs_ = 0; |
| browser_signal_desireability_ = 1; |
| seller_timeout_ = absl::nullopt; |
| browser_signal_highest_scoring_other_bid_ = 0; |
| browser_signal_highest_scoring_other_bid_currency_ = absl::nullopt; |
| } |
| |
| // Configures `url_loader_factory_` to return a script with the specified |
| // return line, expecting the provided result. |
| void RunScoreAdWithReturnValueExpectingResult( |
| const std::string& raw_return_value, |
| double expected_score, |
| const std::vector<std::string>& expected_errors = |
| std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| expected_component_auction_modified_bid_params = |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| absl::optional<uint32_t> expected_data_version = {}, |
| const absl::optional<GURL>& expected_debug_loss_report_url = |
| absl::nullopt, |
| const absl::optional<GURL>& expected_debug_win_report_url = absl::nullopt, |
| mojom::RejectReason expected_reject_reason = |
| mojom::RejectReason::kNotAvailable, |
| PrivateAggregationRequests expected_pa_requests = {}, |
| absl::optional<double> expected_bid_in_seller_currency = absl::nullopt) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript(raw_return_value), expected_score, expected_errors, |
| std::move(expected_component_auction_modified_bid_params), |
| expected_data_version, expected_debug_loss_report_url, |
| expected_debug_win_report_url, expected_reject_reason, |
| std::move(expected_pa_requests), expected_bid_in_seller_currency); |
| } |
| |
| // Behaves just like RunScoreAdWithReturnValueExpectingResult(), but |
| // additionally expects the auction to take exactly `expected_duration`, using |
| // FastForwardBy() to advance time. Can't just use RunLoop and Time::Now() |
| // time, because that can get confused by superfluous events and wait 30 |
| // seconds too long (perhaps confused by the 30 second download timer, even |
| // though the download should complete immediately, and the URLLoader with the |
| // timer on it deleted?) |
| void RunScoreAdWithReturnValueExpectingResultInExactTime( |
| const std::string& raw_return_value, |
| double expected_score, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| expected_component_auction_modified_bid_params, |
| base::TimeDelta expected_duration, |
| const std::vector<std::string>& expected_errors = {}, |
| absl::optional<uint32_t> expected_data_version = {}, |
| const absl::optional<GURL>& expected_debug_loss_report_url = |
| absl::nullopt, |
| const absl::optional<GURL>& expected_debug_win_report_url = absl::nullopt, |
| mojom::RejectReason expected_reject_reason = |
| mojom::RejectReason::kNotAvailable, |
| PrivateAggregationRequests expected_pa_requests = {}, |
| absl::optional<double> expected_bid_in_seller_currency = absl::nullopt) { |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript(raw_return_value)); |
| auto seller_worklet = CreateWorklet(); |
| |
| base::RunLoop run_loop; |
| RunScoreAdOnWorkletAsync( |
| seller_worklet.get(), expected_score, expected_errors, |
| std::move(expected_component_auction_modified_bid_params), |
| expected_data_version, expected_debug_loss_report_url, |
| expected_debug_win_report_url, expected_reject_reason, |
| std::move(expected_pa_requests), expected_bid_in_seller_currency, |
| run_loop.QuitClosure()); |
| task_environment_.FastForwardBy(expected_duration - kTinyTime); |
| EXPECT_FALSE(run_loop.AnyQuitCalled()); |
| task_environment_.FastForwardBy(kTinyTime); |
| EXPECT_TRUE(run_loop.AnyQuitCalled()); |
| } |
| |
| // Configures `url_loader_factory_` to return the provided script, and then |
| // runs its score_ad() function, expecting the provided result. |
| void RunScoreAdWithJavascriptExpectingResult( |
| const std::string& javascript, |
| double expected_score, |
| const std::vector<std::string>& expected_errors = |
| std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| expected_component_auction_modified_bid_params = |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| absl::optional<uint32_t> expected_data_version = {}, |
| const absl::optional<GURL>& expected_debug_loss_report_url = |
| absl::nullopt, |
| const absl::optional<GURL>& expected_debug_win_report_url = absl::nullopt, |
| mojom::RejectReason expected_reject_reason = |
| mojom::RejectReason::kNotAvailable, |
| PrivateAggregationRequests expected_pa_requests = {}, |
| absl::optional<double> expected_bid_in_seller_currency = absl::nullopt) { |
| SCOPED_TRACE(javascript); |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| javascript); |
| RunScoreAdExpectingResult( |
| expected_score, expected_errors, |
| std::move(expected_component_auction_modified_bid_params), |
| expected_data_version, expected_debug_loss_report_url, |
| expected_debug_win_report_url, expected_reject_reason, |
| std::move(expected_pa_requests), expected_bid_in_seller_currency); |
| } |
| |
| // Runs score_ad() script, checking result and invoking provided closure |
| // when done. Something else must spin the event loop. |
| void RunScoreAdOnWorkletAsync( |
| mojom::SellerWorklet* seller_worklet, |
| double expected_score, |
| const std::vector<std::string>& expected_errors, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| expected_component_auction_modified_bid_params, |
| absl::optional<uint32_t> expected_data_version, |
| const absl::optional<GURL>& expected_debug_loss_report_url, |
| const absl::optional<GURL>& expected_debug_win_report_url, |
| mojom::RejectReason expected_reject_reason, |
| PrivateAggregationRequests expected_pa_requests, |
| absl::optional<double> expected_bid_in_seller_currency, |
| base::OnceClosure done_closure) { |
| seller_worklet->ScoreAd( |
| ad_metadata_, bid_, bid_currency_, auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), component_expect_bid_currency_, |
| browser_signal_interest_group_owner_, browser_signal_render_url_, |
| browser_signal_ad_components_, browser_signal_bidding_duration_msecs_, |
| seller_timeout_, |
| /*trace_id=*/1, |
| TestScoreAdClient::Create(base::BindOnce( |
| [](double expected_score, |
| mojom::RejectReason expected_reject_reason, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| expected_component_auction_modified_bid_params, |
| absl::optional<uint32_t> expected_data_version, |
| const absl::optional<GURL>& expected_debug_loss_report_url, |
| const absl::optional<GURL>& expected_debug_win_report_url, |
| PrivateAggregationRequests expected_pa_requests, |
| absl::optional<double> expected_bid_in_seller_currency, |
| std::vector<std::string> expected_errors, |
| base::OnceClosure done_closure, double score, |
| mojom::RejectReason reject_reason, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| component_auction_modified_bid_params, |
| absl::optional<double> bid_in_seller_currency, |
| absl::optional<uint32_t> scoring_signals_data_version, |
| const absl::optional<GURL>& debug_loss_report_url, |
| const absl::optional<GURL>& debug_win_report_url, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) { |
| EXPECT_EQ(expected_score, score); |
| EXPECT_EQ(static_cast<int>(expected_reject_reason), |
| static_cast<int>(reject_reason)); |
| EXPECT_EQ( |
| expected_component_auction_modified_bid_params.is_null(), |
| component_auction_modified_bid_params.is_null()); |
| if (!expected_component_auction_modified_bid_params.is_null() && |
| !component_auction_modified_bid_params.is_null()) { |
| EXPECT_EQ(expected_component_auction_modified_bid_params->ad, |
| component_auction_modified_bid_params->ad); |
| EXPECT_EQ( |
| expected_component_auction_modified_bid_params->has_bid, |
| component_auction_modified_bid_params->has_bid); |
| if (expected_component_auction_modified_bid_params->has_bid) { |
| EXPECT_EQ(expected_component_auction_modified_bid_params->bid, |
| component_auction_modified_bid_params->bid); |
| } |
| } |
| EXPECT_EQ(expected_debug_loss_report_url, debug_loss_report_url); |
| EXPECT_EQ(expected_debug_win_report_url, debug_win_report_url); |
| EXPECT_EQ(expected_data_version, scoring_signals_data_version); |
| EXPECT_EQ(expected_bid_in_seller_currency, |
| bid_in_seller_currency); |
| EXPECT_EQ(expected_pa_requests, pa_requests); |
| EXPECT_EQ(expected_errors, errors); |
| std::move(done_closure).Run(); |
| }, |
| expected_score, expected_reject_reason, |
| std::move(expected_component_auction_modified_bid_params), |
| expected_data_version, expected_debug_loss_report_url, |
| expected_debug_win_report_url, std::move(expected_pa_requests), |
| expected_bid_in_seller_currency, expected_errors, |
| std::move(done_closure)))); |
| } |
| |
| void RunScoreAdOnWorkletExpectingCallbackNeverInvoked( |
| mojom::SellerWorklet* seller_worklet) { |
| seller_worklet->ScoreAd( |
| ad_metadata_, bid_, bid_currency_, auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), component_expect_bid_currency_, |
| browser_signal_interest_group_owner_, browser_signal_render_url_, |
| browser_signal_ad_components_, browser_signal_bidding_duration_msecs_, |
| seller_timeout_, |
| /*trace_id=*/1, |
| TestScoreAdClient::Create( |
| TestScoreAdClient::ScoreAdNeverInvokedCallback())); |
| } |
| |
| // Loads and runs a scode_ad() script, expecting the supplied result. |
| void RunScoreAdExpectingResultOnWorklet( |
| mojom::SellerWorklet* seller_worklet, |
| double expected_score, |
| const std::vector<std::string>& expected_errors = |
| std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| expected_component_auction_modified_bid_params = |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| absl::optional<uint32_t> expected_data_version = absl::nullopt, |
| const absl::optional<GURL>& expected_debug_loss_report_url = |
| absl::nullopt, |
| const absl::optional<GURL>& expected_debug_win_report_url = absl::nullopt, |
| mojom::RejectReason expected_reject_reason = |
| mojom::RejectReason::kNotAvailable, |
| PrivateAggregationRequests expected_pa_requests = {}, |
| absl::optional<double> expected_bid_in_seller_currency = absl::nullopt) { |
| base::RunLoop run_loop; |
| RunScoreAdOnWorkletAsync( |
| seller_worklet, expected_score, expected_errors, |
| std::move(expected_component_auction_modified_bid_params), |
| expected_data_version, expected_debug_loss_report_url, |
| expected_debug_win_report_url, expected_reject_reason, |
| std::move(expected_pa_requests), expected_bid_in_seller_currency, |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| // Loads and runs a scode_ad() script, expecting the supplied result. |
| void RunScoreAdExpectingResult( |
| double expected_score, |
| const std::vector<std::string>& expected_errors = |
| std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| expected_component_auction_modified_bid_params = |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| absl::optional<uint32_t> expected_data_version = absl::nullopt, |
| const absl::optional<GURL>& expected_debug_loss_report_url = |
| absl::nullopt, |
| const absl::optional<GURL>& expected_debug_win_report_url = absl::nullopt, |
| mojom::RejectReason expected_reject_reason = |
| mojom::RejectReason::kNotAvailable, |
| PrivateAggregationRequests expected_pa_requests = {}, |
| absl::optional<double> expected_bid_in_seller_currency = absl::nullopt) { |
| auto seller_worklet = CreateWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| RunScoreAdExpectingResultOnWorklet( |
| seller_worklet.get(), expected_score, expected_errors, |
| std::move(expected_component_auction_modified_bid_params), |
| expected_data_version, expected_debug_loss_report_url, |
| expected_debug_win_report_url, expected_reject_reason, |
| std::move(expected_pa_requests), expected_bid_in_seller_currency); |
| } |
| |
| // Configures `url_loader_factory_` to return a report_result() script created |
| // with CreateReportToScript(). Then runs the script, expecting the provided |
| // result. |
| void RunReportResultCreatedScriptExpectingResult( |
| const std::string& raw_return_value, |
| const std::string& extra_code, |
| const absl::optional<std::string>& expected_signals_for_winner, |
| const absl::optional<GURL>& expected_report_url, |
| const base::flat_map<std::string, GURL>& expected_ad_beacon_map = |
| base::flat_map<std::string, GURL>(), |
| PrivateAggregationRequests expected_pa_requests = {}, |
| const std::vector<std::string>& expected_errors = |
| std::vector<std::string>()) { |
| RunReportResultWithJavascriptExpectingResult( |
| CreateReportToScript(raw_return_value, extra_code), |
| expected_signals_for_winner, expected_report_url, |
| expected_ad_beacon_map, std::move(expected_pa_requests), |
| expected_errors); |
| } |
| |
| // Configures `url_loader_factory_` to return the provided script, and then |
| // runs its report_result() function. Then runs the script, expecting the |
| // provided result. |
| void RunReportResultWithJavascriptExpectingResult( |
| const std::string& javascript, |
| const absl::optional<std::string>& expected_signals_for_winner, |
| const absl::optional<GURL>& expected_report_url, |
| const base::flat_map<std::string, GURL>& expected_ad_beacon_map = |
| base::flat_map<std::string, GURL>(), |
| PrivateAggregationRequests expected_pa_requests = {}, |
| const std::vector<std::string>& expected_errors = |
| std::vector<std::string>()) { |
| SCOPED_TRACE(javascript); |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| javascript); |
| RunReportResultExpectingResult(expected_signals_for_winner, |
| expected_report_url, expected_ad_beacon_map, |
| std::move(expected_pa_requests), |
| expected_errors); |
| } |
| |
| // Loads and runs a report_result() script, expecting the supplied result. |
| // Caller is responsible for spinning the event loop at least until |
| // `done_closure`. |
| void RunReportResultExpectingResultAsync( |
| mojom::SellerWorklet* seller_worklet, |
| const absl::optional<std::string>& expected_signals_for_winner, |
| const absl::optional<GURL>& expected_report_url, |
| const base::flat_map<std::string, GURL>& expected_ad_beacon_map, |
| PrivateAggregationRequests expected_pa_requests, |
| const std::vector<std::string>& expected_errors, |
| base::OnceClosure done_closure) { |
| seller_worklet->ReportResult( |
| auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), |
| browser_signal_interest_group_owner_, |
| browser_signal_buyer_and_seller_reporting_id_, |
| browser_signal_render_url_, bid_, bid_currency_, |
| browser_signal_desireability_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_highest_scoring_other_bid_currency_, |
| browser_signals_component_auction_report_result_params_.Clone(), |
| browser_signal_data_version_.value_or(0), |
| browser_signal_data_version_.has_value(), |
| /*trace_id=*/1, |
| base::BindOnce( |
| [](const absl::optional<std::string>& expected_signals_for_winner, |
| const absl::optional<GURL>& expected_report_url, |
| const base::flat_map<std::string, GURL>& expected_ad_beacon_map, |
| PrivateAggregationRequests expected_pa_requests, |
| const std::vector<std::string>& expected_errors, |
| base::OnceClosure done_closure, |
| const absl::optional<std::string>& signals_for_winner, |
| const absl::optional<GURL>& report_url, |
| const base::flat_map<std::string, GURL>& ad_beacon_map, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) { |
| if (signals_for_winner && expected_signals_for_winner) { |
| // If neither is null, used fancy base::Value comparison, which |
| // removes dependencies on JSON serialization order and format, |
| // and has better error output. |
| EXPECT_THAT(base::test::ParseJson(*signals_for_winner), |
| base::test::IsJson(*expected_signals_for_winner)); |
| } else { |
| // Otherwise, just compare the optional strings directly. |
| EXPECT_EQ(expected_signals_for_winner, signals_for_winner); |
| } |
| EXPECT_EQ(expected_report_url, report_url); |
| EXPECT_EQ(expected_ad_beacon_map, ad_beacon_map); |
| EXPECT_EQ(expected_pa_requests, pa_requests); |
| EXPECT_EQ(expected_errors, errors); |
| std::move(done_closure).Run(); |
| }, |
| expected_signals_for_winner, expected_report_url, |
| expected_ad_beacon_map, std::move(expected_pa_requests), |
| expected_errors, std::move(done_closure))); |
| } |
| |
| void RunReportResultExpectingCallbackNeverInvoked( |
| mojom::SellerWorklet* seller_worklet) { |
| seller_worklet->ReportResult( |
| auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), |
| browser_signal_interest_group_owner_, |
| browser_signal_buyer_and_seller_reporting_id_, |
| browser_signal_render_url_, bid_, bid_currency_, |
| browser_signal_desireability_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_highest_scoring_other_bid_currency_, |
| browser_signals_component_auction_report_result_params_.Clone(), |
| browser_signal_data_version_.value_or(0), |
| browser_signal_data_version_.has_value(), |
| /*trace_id=*/1, |
| base::BindOnce( |
| [](const absl::optional<std::string>& signals_for_winner, |
| const absl::optional<GURL>& report_url, |
| const base::flat_map<std::string, GURL>& ad_beacon_map, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) { |
| ADD_FAILURE() << "This should not be invoked"; |
| })); |
| } |
| |
| // Loads and runs a report_result() script, expecting the supplied result. |
| void RunReportResultExpectingResult( |
| const absl::optional<std::string>& expected_signals_for_winner, |
| const absl::optional<GURL>& expected_report_url, |
| const base::flat_map<std::string, GURL>& expected_ad_beacon_map = |
| base::flat_map<std::string, GURL>(), |
| PrivateAggregationRequests expected_pa_requests = {}, |
| const std::vector<std::string>& expected_errors = |
| std::vector<std::string>()) { |
| auto seller_worklet = CreateWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| |
| base::RunLoop run_loop; |
| RunReportResultExpectingResultAsync( |
| seller_worklet.get(), expected_signals_for_winner, expected_report_url, |
| expected_ad_beacon_map, std::move(expected_pa_requests), |
| expected_errors, run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| // Create a seller worklet. If out_seller_worklet_impl is non-null, will also |
| // the stash an actual implementation pointer there. |
| mojo::Remote<mojom::SellerWorklet> CreateWorklet( |
| bool pause_for_debugger_on_start = false, |
| SellerWorklet** out_seller_worklet_impl = nullptr, |
| bool use_alternate_url_loader_factory = false) { |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory; |
| if (use_alternate_url_loader_factory) { |
| alternate_url_loader_factory_.Clone( |
| url_loader_factory.InitWithNewPipeAndPassReceiver()); |
| } else { |
| url_loader_factory_.Clone( |
| url_loader_factory.InitWithNewPipeAndPassReceiver()); |
| } |
| |
| mojo::Remote<mojom::SellerWorklet> seller_worklet; |
| auto seller_worklet_impl = std::make_unique<SellerWorklet>( |
| v8_helper_, std::move(shared_storage_host_remote_), |
| pause_for_debugger_on_start, std::move(url_loader_factory), |
| decision_logic_url_, trusted_scoring_signals_url_, top_window_origin_, |
| permissions_policy_state_.Clone(), experiment_group_id_); |
| auto* seller_worklet_ptr = seller_worklet_impl.get(); |
| mojo::ReceiverId receiver_id = |
| seller_worklets_.Add(std::move(seller_worklet_impl), |
| seller_worklet.BindNewPipeAndPassReceiver()); |
| seller_worklet_ptr->set_close_pipe_callback( |
| base::BindOnce(&SellerWorkletTest::ClosePipeCallback, |
| base::Unretained(this), receiver_id)); |
| seller_worklet.set_disconnect_with_reason_handler(base::BindRepeating( |
| &SellerWorkletTest::OnDisconnectWithReason, base::Unretained(this))); |
| |
| if (out_seller_worklet_impl) |
| *out_seller_worklet_impl = seller_worklet_ptr; |
| return seller_worklet; |
| } |
| |
| // 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) { |
| seller_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(); |
| } |
| |
| base::test::TaskEnvironment task_environment_; |
| |
| // Arguments passed to score_bid() and report_result(). Arguments common to |
| // both of them use the same field. |
| // |
| // NOTE: For each new GURL field, ScoreAdLoadCompletionOrder / |
| // ReportResultLoadCompletionOrder should be updated. |
| std::string ad_metadata_; |
| // `bid_` is a browser signal for report_result(), but a direct parameter for |
| // score_bid(). |
| double bid_; |
| absl::optional<blink::AdCurrency> bid_currency_; |
| GURL decision_logic_url_; |
| absl::optional<GURL> trusted_scoring_signals_url_; |
| blink::AuctionConfig::NonSharedParams auction_ad_config_non_shared_params_; |
| absl::optional<GURL> direct_from_seller_seller_signals_; |
| absl::optional<GURL> direct_from_seller_auction_signals_; |
| url::Origin top_window_origin_; |
| mojom::AuctionWorkletPermissionsPolicyStatePtr permissions_policy_state_; |
| absl::optional<uint16_t> experiment_group_id_; |
| mojom::ComponentAuctionOtherSellerPtr browser_signals_other_seller_; |
| absl::optional<blink::AdCurrency> component_expect_bid_currency_; |
| url::Origin browser_signal_interest_group_owner_; |
| absl::optional<std::string> browser_signal_buyer_and_seller_reporting_id_; |
| GURL browser_signal_render_url_; |
| std::vector<GURL> browser_signal_ad_components_; |
| uint32_t browser_signal_bidding_duration_msecs_; |
| double browser_signal_desireability_; |
| double browser_signal_highest_scoring_other_bid_; |
| absl::optional<blink::AdCurrency> |
| browser_signal_highest_scoring_other_bid_currency_; |
| mojom::ComponentAuctionReportResultParamsPtr |
| browser_signals_component_auction_report_result_params_; |
| absl::optional<uint32_t> browser_signal_data_version_; |
| absl::optional<base::TimeDelta> seller_timeout_; |
| |
| // Reuseable run loop for disconnection errors. |
| std::unique_ptr<base::RunLoop> disconnect_run_loop_; |
| absl::optional<std::string> disconnect_reason_; |
| |
| network::TestURLLoaderFactory url_loader_factory_; |
| network::TestURLLoaderFactory alternate_url_loader_factory_; |
| scoped_refptr<AuctionV8Helper> v8_helper_; |
| |
| mojo::PendingRemote<mojom::AuctionSharedStorageHost> |
| shared_storage_host_remote_; |
| |
| // Owns all created seller worklets - 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::SellerWorklet> seller_worklets_; |
| }; |
| |
| // Test the case the SellerWorklet pipe is closed before any of its methods are |
| // invoked. Nothing should happen. |
| TEST_F(SellerWorkletTest, PipeClosed) { |
| auto sellet_worklet = CreateWorklet(); |
| sellet_worklet.reset(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| TEST_F(SellerWorkletTest, NetworkError) { |
| url_loader_factory_.AddResponse(decision_logic_url_.spec(), |
| CreateBasicSellAdScript(), |
| net::HTTP_NOT_FOUND); |
| auto sellet_worklet = CreateWorklet(); |
| EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.", |
| WaitForDisconnect()); |
| } |
| |
| TEST_F(SellerWorkletTest, CompileError) { |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| "Invalid Javascript"); |
| auto sellet_worklet = CreateWorklet(); |
| std::string disconnect_error = WaitForDisconnect(); |
| EXPECT_THAT(disconnect_error, StartsWith("https://url.test/:1 ")); |
| EXPECT_THAT(disconnect_error, HasSubstr("SyntaxError")); |
| } |
| |
| // Test parsing of return values. |
| TEST_F(SellerWorkletTest, ScoreAd) { |
| // Base case. Also serves to make sure the script returned by |
| // CreateBasicSellAdScript() does indeed work. |
| RunScoreAdWithJavascriptExpectingResult(CreateBasicSellAdScript(), 1); |
| |
| // Test returning results with the object format. |
| RunScoreAdWithReturnValueExpectingResult("{desirability:3}", 3); |
| RunScoreAdWithReturnValueExpectingResult("{desirability:0.5}", 0.5); |
| RunScoreAdWithReturnValueExpectingResult("{desirability:0}", 0); |
| RunScoreAdWithReturnValueExpectingResult("{desirability:-10}", 0); |
| |
| // Test returning a number, which is interpreted as a score. |
| RunScoreAdWithReturnValueExpectingResult("3", 3); |
| RunScoreAdWithReturnValueExpectingResult("0.5", 0.5); |
| RunScoreAdWithReturnValueExpectingResult("0", 0); |
| RunScoreAdWithReturnValueExpectingResult("-10", 0); |
| |
| // Unknown fields should be ignored. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:3, snore:1/0, smore:[15], shore:{desirability:2}}", 3); |
| |
| // No return value. |
| RunScoreAdWithReturnValueExpectingResult( |
| "", 0, |
| {"https://url.test/ scoreAd() did not return an object or a number."}); |
| |
| // Wrong return type / invalid values. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{hats:15}", 0, |
| {"https://url.test/ scoreAd() return value has incorrect structure."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:[15]}", 0, |
| {"https://url.test/ scoreAd() return value has incorrect structure."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1/0}", 0, |
| {"https://url.test/ scoreAd() returned an invalid score."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:0/0}", 0, |
| {"https://url.test/ scoreAd() returned an invalid score."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1/0}", 0, |
| {"https://url.test/ scoreAd() returned an invalid score."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:true}", 0, |
| {"https://url.test/ scoreAd() return value has incorrect structure."}); |
| |
| // Same tests as the previous block, but returning the value directly instead |
| // of in an object. |
| RunScoreAdWithReturnValueExpectingResult( |
| "[15]", 0, |
| {"https://url.test/ scoreAd() return value has incorrect structure."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "1/0", 0, {"https://url.test/ scoreAd() returned an invalid score."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "0/0", 0, {"https://url.test/ scoreAd() returned an invalid score."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "-1/0", 0, {"https://url.test/ scoreAd() returned an invalid score."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "true", 0, |
| {"https://url.test/ scoreAd() did not return an object or a number."}); |
| |
| // Throw exception. |
| RunScoreAdWithReturnValueExpectingResult( |
| "shrimp", 0, |
| {"https://url.test/:5 Uncaught ReferenceError: shrimp is not defined."}); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdAllowComponentAuction) { |
| // Expected errors vector on failure. |
| const std::vector<std::string> kExpectedErrorsOnFailure{ |
| R"(https://url.test/ scoreAd() return value does not have )" |
| R"(allowComponentAuction set to true. Ad dropped from component )" |
| R"(auction.)"}; |
| |
| // Expected ComponentAuctionModifiedBidParams, for successful cases when a |
| // component auction's `scoreAd()` script is simulated. |
| const mojom::ComponentAuctionModifiedBidParamsPtr |
| kExpectedComponentAuctionModifiedBidParams = |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false); |
| |
| // With a null `browser_signals_other_seller_`, returning a raw score is |
| // allowed, and if returning an object, `allowComponentAuction` doesn't |
| // matter. |
| browser_signals_other_seller_.reset(); |
| RunScoreAdWithReturnValueExpectingResult("1", 1); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:true}", 1); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:false}", 1); |
| RunScoreAdWithReturnValueExpectingResult("{desirability:1}", 1); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:1}", 1); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:0}", 1); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:[32]}", 1); |
| |
| // With a top-level seller in `browser_signals_other_seller_`, an object must |
| // be returned, and `allowComponentAuction` must be true. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.seller.test"))); |
| RunScoreAdWithReturnValueExpectingResult("1", 0, kExpectedErrorsOnFailure); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:true}", 1, |
| /*expected_errors=*/{}, |
| kExpectedComponentAuctionModifiedBidParams.Clone()); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:false}", 0, |
| kExpectedErrorsOnFailure); |
| RunScoreAdWithReturnValueExpectingResult("{desirability:1}", 0, |
| kExpectedErrorsOnFailure); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:1}", 1, |
| /*expected_errors=*/{}, |
| kExpectedComponentAuctionModifiedBidParams.Clone()); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:0}", 0, kExpectedErrorsOnFailure); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:[32]}", 1, |
| /*expected_errors=*/{}, |
| kExpectedComponentAuctionModifiedBidParams.Clone()); |
| // Even with a desirability of 0, a false `allowComponentAuction` value is |
| // considered an error. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:0, allowComponentAuction:false}", 0, |
| kExpectedErrorsOnFailure); |
| // A missing `allowComponentAuction` field is treated as if it were "false". |
| RunScoreAdWithReturnValueExpectingResult("{desirability:1}", 0, |
| kExpectedErrorsOnFailure); |
| |
| // With a component seller in `browser_signals_other_seller_`, an object must |
| // be returned, and `allowComponentAuction` must be true. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.seller.test"))); |
| RunScoreAdWithReturnValueExpectingResult("1", 0, kExpectedErrorsOnFailure); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:true}", 1); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:false}", 0, |
| kExpectedErrorsOnFailure); |
| RunScoreAdWithReturnValueExpectingResult("{desirability:1}", 0, |
| kExpectedErrorsOnFailure); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:1}", 1); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:0}", 0, kExpectedErrorsOnFailure); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:[32]}", 1); |
| } |
| |
| // Test the `ad` output field of scoreAd(). |
| TEST_F(SellerWorkletTest, ScoreAdAd) { |
| // When `browser_signals_other_seller_` is not top a top-level auction (i.e. |
| // scoreAd() is invoked for a top-level seller, so the other seller is empty |
| // or a component seller), `ad` field is ignored, and a null |
| // ComponentAuctionModifiedBidParamsPtr is returned. |
| browser_signals_other_seller_.reset(); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1}", 1, |
| /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr()); |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.seller.test"))); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true}", 1, |
| /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr()); |
| |
| // When `browser_signals_other_seller_` is not top a top-level auction, a |
| // ComponentAuctionModifiedBidParamsPtr with an `ad` field is returned. |
| |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.seller.test"))); |
| // If the ad field isn't present, `ad` is set to "null". |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, allowComponentAuction:true}", 1, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true}", 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"({ad:"foo", desirability:1, allowComponentAuction:true})", 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/R"("foo")", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:[[35]], desirability:1, allowComponentAuction:true}", 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"[[35]]", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| } |
| |
| // Test the `rejectReason` output field of scoreAd(). |
| TEST_F(SellerWorkletTest, ScoreAdRejectReason) { |
| const struct { |
| std::string reason_str; |
| mojom::RejectReason reason_enum; |
| } kTestCases[] = { |
| {"not-available", mojom::RejectReason::kNotAvailable}, |
| {"invalid-bid", mojom::RejectReason::kInvalidBid}, |
| {"bid-below-auction-floor", mojom::RejectReason::kBidBelowAuctionFloor}, |
| {"pending-approval-by-exchange", |
| mojom::RejectReason::kPendingApprovalByExchange}, |
| {"disapproved-by-exchange", mojom::RejectReason::kDisapprovedByExchange}, |
| {"blocked-by-publisher", mojom::RejectReason::kBlockedByPublisher}, |
| {"language-exclusions", mojom::RejectReason::kLanguageExclusions}, |
| {"category-exclusions", mojom::RejectReason::kCategoryExclusions}, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| RunScoreAdWithReturnValueExpectingResult( |
| base::StringPrintf(R"({desirability:-1, rejectReason: '%s'})", |
| test_case.reason_str.c_str()), |
| 0, |
| /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, test_case.reason_enum); |
| } |
| |
| // Default reject reason is mojom::RejectReason::kNotAvailable, if scoreAd() |
| // does not return one. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:-1}", 0, |
| /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| mojom::RejectReason::kNotAvailable); |
| |
| // Reject reason is ignored when desirability is positive. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:3, rejectReason: 'invalid-bid'}", 3, |
| /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason*/ mojom::RejectReason::kNotAvailable); |
| } |
| |
| // Invalid `rejectReason` output of scoreAd() results in error. |
| TEST_F(SellerWorkletTest, ScoreAdInvalidRejectReason) { |
| // Reject reason string returned by seller script is case sensitive. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:-1, rejectReason: 'INVALID-BID'}", 0, |
| /*expected_errors=*/ |
| {"https://url.test/ scoreAd() returned an invalid reject reason."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason*/ mojom::RejectReason::kNotAvailable); |
| |
| // Reject reason returned by seller script must be a string. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:-1, rejectReason: 2}", 0, |
| /*expected_errors=*/ |
| {"https://url.test/ rejectReason returned by scoreAd() must be a " |
| "string."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason*/ mojom::RejectReason::kNotAvailable); |
| } |
| |
| // Test the `bid` output field of scoreAd(). |
| TEST_F(SellerWorkletTest, ScoreAdModifiesBid) { |
| // When `browser_signals_other_seller_` is not top a top-level auction (i.e. |
| // scoreAd() is invoked for a top-level seller), `bid` field is ignored. |
| browser_signals_other_seller_.reset(); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{bid:5, desirability:1}", 1, |
| /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr()); |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.seller.test"))); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{bid:10, desirability:1, allowComponentAuction:true}", 1, |
| /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr()); |
| |
| // Otherwise, bid field is returned to the caller. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.level.seller.test"))); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, bid:13}", 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/13, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/true)); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, bid:1.2}", 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/1.2, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/true)); |
| |
| // Can also specify the currency. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, " |
| "bid:1.2, bidCurrency: 'USD'}", |
| 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/1.2, |
| /*bid_currency=*/blink::AdCurrency::From("USD"), |
| /*has_bid=*/true)); |
| |
| // Not providing a bid is fine. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true}", 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| |
| // Non-numeric bids are ignored. |
| RunScoreAdWithReturnValueExpectingResult( |
| R"({ad:null, desirability:1, allowComponentAuction:true, bid:"5"})", 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, bid:[4]}", 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| |
| // Invalid bids result in errors. |
| RunScoreAdWithReturnValueExpectingResult( |
| R"({ad:null, desirability:1, allowComponentAuction:true, bid:0})", 0, |
| /*expected_errors=*/ |
| {"https://url.test/ scoreAd() returned an invalid bid."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"({ad:null, desirability:1, allowComponentAuction:true, bid:-1})", 0, |
| /*expected_errors=*/ |
| {"https://url.test/ scoreAd() returned an invalid bid."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, bid:1/0}", 0, |
| /*expected_errors=*/ |
| {"https://url.test/ scoreAd() returned an invalid bid."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, bid:-1/0}", 0, |
| /*expected_errors=*/ |
| {"https://url.test/ scoreAd() returned an invalid bid."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, bid:0/0}", 0, |
| /*expected_errors=*/ |
| {"https://url.test/ scoreAd() returned an invalid bid."}); |
| |
| // Currency mismatch or invalid currency produce errors, too |
| // (and a match doesn't) |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, " |
| "bid:1.2, bidCurrency: 'USSD'}", |
| 0, {"https://url.test/ scoreAd() returned an invalid bidCurrency."}); |
| auction_ad_config_non_shared_params_.seller_currency = |
| blink::AdCurrency::From("CAD"); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, " |
| "bid:1.2, bidCurrency: 'USD'}", |
| 0, |
| {"https://url.test/ scoreAd() bidCurrency mismatch vs own sellerCurrency," |
| " expected 'CAD' got 'USD'."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, " |
| "bid:1.2, bidCurrency: 'CAD'}", |
| 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/1.2, |
| /*bid_currency=*/blink::AdCurrency::From("CAD"), |
| /*has_bid=*/true)); |
| auction_ad_config_non_shared_params_.seller_currency = absl::nullopt; |
| |
| // In component auctions, there is also verification against parent auction's |
| // bidderCurrencies. |
| component_expect_bid_currency_ = blink::AdCurrency::From("EUR"); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, " |
| "bid:1.2, bidCurrency: 'USD'}", |
| 0, |
| {"https://url.test/ scoreAd() bidCurrency mismatch in component auction " |
| "vs parent auction bidderCurrency, expected 'EUR' got 'USD'."}); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:1, allowComponentAuction:true, " |
| "bid:1.2, bidCurrency: 'EUR'}", |
| 1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/1.2, |
| /*bid_currency=*/blink::AdCurrency::From("EUR"), |
| /*has_bid=*/true)); |
| component_expect_bid_currency_ = absl::nullopt; |
| |
| // A 0 bid is normally considered invalid, unless desirability is 0, in which |
| // case it is ignored. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{ad:null, desirability:0, allowComponentAuction:true, bid:0}", 0); |
| } |
| |
| // Test the `incomingBidInSellerCurrency` output field of scoreAd() |
| TEST_F(SellerWorkletTest, ScoreAdIncomingBidInSellerCurrency) { |
| // Configure bid currency to make sure checks for passthrough are done. |
| bid_currency_ = blink::AdCurrency::From("USD"); |
| |
| // If seller currency isn't configured, can't set it. |
| auction_ad_config_non_shared_params_.seller_currency = absl::nullopt; |
| |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, incomingBidInSellerCurrency: 4}", 0, |
| {"https://url.test/ scoreAd() attempting to set " |
| "incomingBidInSellerCurrency without a configured sellerCurrency."}); |
| |
| auction_ad_config_non_shared_params_.seller_currency = |
| blink::AdCurrency::From("CAD"); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, incomingBidInSellerCurrency: 'foo'}", 0, |
| {"https://url.test/ scoreAd() incomingBidInSellerCurrency not " |
| "a number."}); |
| |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, incomingBidInSellerCurrency: -100}", 0, |
| {"https://url.test/ scoreAd() incomingBidInSellerCurrency not " |
| "a valid bid."}); |
| |
| // Now pass in a valid one. |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, incomingBidInSellerCurrency: 100}", 1, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/100); |
| |
| // When bid currency matches seller currency, incomingBidInSellerCurrency |
| // should only be be accepted if it matches the incoming value. |
| bid_currency_ = blink::AdCurrency::From("CAD"); |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, incomingBidInSellerCurrency: 100}", 0, |
| {"https://url.test/ scoreAd() attempting to set " |
| "incomingBidInSellerCurrency inconsistent with incoming bid already in " |
| "seller currency."}); |
| |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1, incomingBidInSellerCurrency: 1}", 1, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/1); |
| |
| // ...can also have that same-currency bid directly forwarded. |
| bid_ = 3.14; |
| RunScoreAdWithReturnValueExpectingResult( |
| "{desirability:1}", 1, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/3.14); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdDateNotAvailable) { |
| RunScoreAdWithReturnValueExpectingResult( |
| "Date.parse(Date().toString())", 0, |
| {"https://url.test/:5 Uncaught ReferenceError: Date is not defined."}); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdMedata) { |
| ad_metadata_ = R"("foo")"; |
| RunScoreAdWithReturnValueExpectingResult(R"(adMetadata === "foo" ? 4 : 0)", |
| 4); |
| |
| ad_metadata_ = "[1]"; |
| RunScoreAdWithReturnValueExpectingResult(R"(adMetadata[0] === 1 ? 4 : 0)", 4); |
| |
| // If adMetadata is invalid, score should be 0. |
| ad_metadata_ = "{invalid_json"; |
| RunScoreAdWithReturnValueExpectingResult("1", 0); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdTopWindowOrigin) { |
| top_window_origin_ = url::Origin::Create(GURL("https://foo.test/")); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.topWindowHostname == "foo.test" ? 2 : 0)", 2); |
| |
| top_window_origin_ = url::Origin::Create(GURL("https://[::1]:40000/")); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.topWindowHostname == "[::1]" ? 3 : 0)", 3); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdTopLevelSeller) { |
| // `topLevelSeller` should be empty when a top-level seller is scoring a bid |
| // from its own auction. |
| browser_signals_other_seller_.reset(); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"("topLevelSeller" in browserSignals ? 0 : 1)", 1); |
| |
| // `topLevelSeller` should be set when a top-level seller is scoring a bid |
| // from a component auction. Must set `allowComponentAuction` to true for any |
| // value to be returned. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.seller.test"))); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.topLevelSeller === "https://top.seller.test" ? |
| {desirability: 2, allowComponentAuction: true} : 0)", |
| 2, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| |
| // `topLevelSeller` should be empty when a component seller is scoring a bid. |
| // Must set `allowComponentAuction` to true for any value to be returned. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.test"))); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"("topLevelSeller" in browserSignals ? |
| 0 : {desirability: 3, allowComponentAuction: true})", |
| 3); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdComponentSeller) { |
| // `componentSeller` should be empty when a top-level seller is scoring a bid |
| // from its own auction. |
| browser_signals_other_seller_.reset(); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"("componentSeller" in browserSignals ? 0 : 1)", 1); |
| |
| // `componentSeller` should be empty when a top-level seller is scoring a bid |
| // from a component auction. Must set `allowComponentAuction` to true for any |
| // value to be returned. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.seller.test"))); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"("componentSeller" in browserSignals ? |
| 0 : {desirability: 2, allowComponentAuction: true})", |
| 2, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParams::New( |
| /*ad=*/"null", /*bid=*/0, /*bid_currency=*/absl::nullopt, |
| /*has_bid=*/false)); |
| |
| // `componentSeller` should be set when a component seller is scoring a bid. |
| // Must set `allowComponentAuction` to true for any value to be returned. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.test"))); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.componentSeller === "https://component.test" ? |
| {desirability: 3, allowComponentAuction: true} : 0)", |
| 3); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdInterestGroupOwner) { |
| browser_signal_interest_group_owner_ = |
| url::Origin::Create(GURL("https://foo.test/")); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.interestGroupOwner == "https://foo.test" ? 2 : 0)", 2); |
| |
| browser_signal_interest_group_owner_ = |
| url::Origin::Create(GURL("https://[::1]:40000/")); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.interestGroupOwner == "https://[::1]:40000" ? 3 : 0)", |
| 3); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdRenderUrl) { |
| browser_signal_render_url_ = GURL("https://bar.test/path"); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.renderUrl === "https://bar.test/path" ? 3 : 0)", 3); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.renderUrl === "https://bar.test/" ? 3 : 0)", 0); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdAdComponents) { |
| browser_signal_ad_components_.clear(); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.adComponents === undefined ? 3 : 0)", 3); |
| |
| browser_signal_ad_components_ = {GURL("https://bar.test/path")}; |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.adComponents.length)", 1); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.adComponents[0] === "https://bar.test/path" ? 3 : 0)", |
| 3); |
| |
| // These are not in lexical order to make sure ordering is preserved. |
| browser_signal_ad_components_ = {GURL("https://2.test/"), |
| GURL("https://1.test/"), |
| GURL("https://3.test/")}; |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.adComponents.length)", 3); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.adComponents[0] === "https://2.test/" ? 3 : 0)", 3); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.adComponents[1] === "https://1.test/" ? 3 : 0)", 3); |
| RunScoreAdWithReturnValueExpectingResult( |
| R"(browserSignals.adComponents[2] === "https://3.test/" ? 3 : 0)", 3); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdBid) { |
| bid_ = 5; |
| RunScoreAdWithReturnValueExpectingResult("bid", 5); |
| bid_ = 0.5; |
| RunScoreAdWithReturnValueExpectingResult("bid", 0.5); |
| bid_ = -1; |
| RunScoreAdWithReturnValueExpectingResult("bid", 0); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdBidCurrency) { |
| RunScoreAdWithReturnValueExpectingResult( |
| "browserSignals.bidCurrency === '???' ? 2 : 0", 2); |
| |
| bid_currency_ = blink::AdCurrency::From("USD"); |
| RunScoreAdWithReturnValueExpectingResult( |
| "browserSignals.bidCurrency === 'USD' ? 2 : 0", 2); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdBiddingDuration) { |
| // Test browserSignals.bidding_duration_msec. |
| browser_signal_bidding_duration_msecs_ = 0; |
| RunScoreAdWithReturnValueExpectingResult("browserSignals.biddingDurationMsec", |
| 0); |
| browser_signal_bidding_duration_msecs_ = 100; |
| RunScoreAdWithReturnValueExpectingResult("browserSignals.biddingDurationMsec", |
| 100); |
| } |
| |
| // Test that auction config gets into scoreAd. More detailed handling of |
| // (shared) construction of actual object is in ReportResultAuctionConfigParam, |
| // as that worklet is easier to get things out of. |
| TEST_F(SellerWorkletTest, ScoreAdAuctionConfigParam) { |
| decision_logic_url_ = GURL("https://url.test/"); |
| RunScoreAdWithReturnValueExpectingResult( |
| "auctionConfig.decisionLogicUrl.length", |
| decision_logic_url_.spec().length()); |
| |
| decision_logic_url_ = GURL("https://url.test/longer/url"); |
| RunScoreAdWithReturnValueExpectingResult( |
| "auctionConfig.decisionLogicUrl.length", |
| decision_logic_url_.spec().length()); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdExperimentGroupIdParam) { |
| RunScoreAdWithReturnValueExpectingResult( |
| R"("experimentGroupId" in auctionConfig ? 1 : 0)", 0); |
| |
| experiment_group_id_ = 954u; |
| RunScoreAdWithReturnValueExpectingResult("auctionConfig.experimentGroupId", |
| 954); |
| } |
| |
| // Tests that trusted scoring signals are correctly passed to scoreAd(). Each |
| // request is sent individually, without calling SendPendingSignalsRequests() - |
| // instead, the test advances the mock clock by |
| // TrustedSignalsRequestManager::kAutoSendDelay, triggering each request to |
| // automatically be sent. |
| TEST_F(SellerWorkletTest, ScoreAdTrustedScoringSignals) { |
| // With no trusted scoring signals URL, `trustedScoringSignals` should be |
| // null. |
| trusted_scoring_signals_url_ = absl::nullopt; |
| RunScoreAdWithReturnValueExpectingResult( |
| "trustedScoringSignals === null ? 1 : 0", 1); |
| |
| trusted_scoring_signals_url_ = |
| GURL("https://url.test/trusted_scoring_signals"); |
| // Trusted scoring signals URL without any component ads. |
| const GURL kNoComponentSignalsUrl = GURL( |
| "https://url.test/trusted_scoring_signals?hostname=window.test" |
| "&renderUrls=https%3A%2F%2Frender.url.test%2F"); |
| |
| // Successful download case. |
| |
| AddVersionedJsonResponse(&url_loader_factory_, kNoComponentSignalsUrl, |
| kTrustedScoringSignalsResponse, /*data_version=*/1); |
| |
| // Each call should cause the clock to advance exactly `kAutoSendDelay` |
| // milliseconds before the request is send over the wire, waiting for other |
| // requests. |
| RunScoreAdWithReturnValueExpectingResultInExactTime( |
| "trustedScoringSignals.renderUrl['https://render.url.test/']", |
| 4 /* Magic value in trustedScoringSignals */, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| TrustedSignalsRequestManager::kAutoSendDelay, /*expected_errors=*/{}, |
| /*expected_data_version=*/1); |
| RunScoreAdWithReturnValueExpectingResultInExactTime( |
| "trustedScoringSignals.adComponentRenderUrls === undefined ? 1 : 0", 1, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| TrustedSignalsRequestManager::kAutoSendDelay, /*expected_errors=*/{}, |
| /*expected_data_version=*/1); |
| |
| // A network error when fetching the scoring signals results in null |
| // `trustedScoringSignals`. This case is just before the component ad test |
| // case so that its error response for `kNoComponentSignalsUrl` makes a |
| // failure if that URL is incorrectly requested in the component ad test case. |
| url_loader_factory_.AddResponse(kNoComponentSignalsUrl.spec(), |
| /*content=*/std::string(), |
| net::HTTP_NOT_FOUND); |
| RunScoreAdWithReturnValueExpectingResultInExactTime( |
| "trustedScoringSignals === null ? 1 : 0", 1, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| TrustedSignalsRequestManager::kAutoSendDelay, |
| /*expected_errors=*/ |
| {base::StringPrintf("Failed to load %s HTTP status = 404 Not Found.", |
| kNoComponentSignalsUrl.spec().c_str())}); |
| |
| browser_signal_ad_components_ = {GURL("https://component1.test/"), |
| GURL("https://component2.test/")}; |
| AddVersionedJsonResponse( |
| &url_loader_factory_, |
| GURL("https://url.test/trusted_scoring_signals?hostname=window.test" |
| "&renderUrls=https%3A%2F%2Frender.url.test%2F" |
| "&adComponentRenderUrls=https%3A%2F%2Fcomponent1.test%2F," |
| "https%3A%2F%2Fcomponent2.test%2F"), |
| kTrustedScoringSignalsResponse, /*data_version=*/5); |
| |
| RunScoreAdWithReturnValueExpectingResultInExactTime( |
| "trustedScoringSignals.renderUrl['https://render.url.test/']", |
| 4 /* Magic value in trustedScoringSignals */, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| TrustedSignalsRequestManager::kAutoSendDelay, /*expected_errors=*/{}, |
| /*expected_data_version=*/5); |
| RunScoreAdWithReturnValueExpectingResultInExactTime( |
| "trustedScoringSignals.adComponentRenderUrls['https://component1.test/']", |
| 1 /* Magic value in trustedScoringSignals */, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| TrustedSignalsRequestManager::kAutoSendDelay, /*expected_errors=*/{}, |
| /*expected_data_version=*/5); |
| RunScoreAdWithReturnValueExpectingResultInExactTime( |
| "trustedScoringSignals.adComponentRenderUrls['https://component2.test/']", |
| 2 /* Magic value in trustedScoringSignals */, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| TrustedSignalsRequestManager::kAutoSendDelay, /*expected_errors=*/{}, |
| /*expected_data_version=*/5); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdDataVersion) { |
| trusted_scoring_signals_url_ = |
| GURL("https://url.test/trusted_scoring_signals"); |
| // Trusted scoring signals URL without any component ads. |
| const GURL kNoComponentSignalsUrl = GURL( |
| "https://url.test/trusted_scoring_signals?hostname=window.test" |
| "&renderUrls=https%3A%2F%2Frender.url.test%2F"); |
| |
| // Successful download case. |
| AddVersionedJsonResponse(&url_loader_factory_, kNoComponentSignalsUrl, |
| kTrustedScoringSignalsResponse, |
| /*data_version=*/100); |
| RunScoreAdWithReturnValueExpectingResult( |
| "browserSignals.dataVersion", 100, |
| /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/100); |
| } |
| |
| TEST_F(SellerWorkletTest, ScoreAdExperimentGroupId) { |
| experiment_group_id_ = 3948u; |
| trusted_scoring_signals_url_ = |
| GURL("https://url.test/trusted_scoring_signals"); |
| // Trusted scoring signals URL without any component ads and the above |
| // experiment group id. |
| const GURL kSignalsUrl = GURL( |
| "https://url.test/trusted_scoring_signals?hostname=window.test" |
| "&renderUrls=https%3A%2F%2Frender.url.test%2F" |
| "&experimentGroupId=3948"); |
| |
| // The experiment ID is also passed in in auction config. |
| AddJsonResponse(&url_loader_factory_, kSignalsUrl, |
| kTrustedScoringSignalsResponse); |
| RunScoreAdWithReturnValueExpectingResult("auctionConfig.experimentGroupId", |
| 3948, |
| /*expected_errors=*/{}); |
| } |
| |
| // Test the case of a bunch of ScoreAd() calls in parallel, all started before |
| // the worklet script has loaded. |
| TEST_F(SellerWorkletTest, ScoreAdParallelBeforeLoadComplete) { |
| auto seller_worklet = CreateWorklet(/*pause_for_debugger_on_start=*/false); |
| |
| const size_t kNumWorklets = 10; |
| size_t num_completed_worklets = 0; |
| base::RunLoop run_loop; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| browser_signal_render_url_ = GURL(base::StringPrintf("https://foo/%zu", i)); |
| RunScoreAdOnWorkletAsync(seller_worklet.get(), /*expected_score=*/i, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| base::BindLambdaForTesting([&]() { |
| ++num_completed_worklets; |
| if (num_completed_worklets == kNumWorklets) |
| run_loop.Quit(); |
| })); |
| } |
| |
| // No calls should complete, since the script hasn't loaded yet. |
| task_environment_.RunUntilIdle(); |
| EXPECT_EQ(0u, num_completed_worklets); |
| |
| // Load a seller script that uses the last character of `renderUrl` as the |
| // score. The worklet should report a successful load. |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript("parseInt(browserSignals.renderUrl.slice(-1))")); |
| |
| // All scripts should complete successfully. |
| run_loop.Run(); |
| } |
| |
| // Test the case of a bunch of ScoreAd() calls in parallel, all started after |
| // the worklet script has loaded. |
| TEST_F(SellerWorkletTest, ScoreAdParallelAfterLoadComplete) { |
| // Seller script that uses the last character of `renderUrl` as the score. |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript("parseInt(browserSignals.renderUrl.slice(-1))")); |
| auto seller_worklet = CreateWorklet(); |
| |
| const size_t kNumWorklets = 10; |
| size_t num_completed_worklets = 0; |
| base::RunLoop run_loop; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| browser_signal_render_url_ = GURL(base::StringPrintf("https://foo/%zu", i)); |
| RunScoreAdOnWorkletAsync(seller_worklet.get(), /*expected_score=*/i, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| base::BindLambdaForTesting([&]() { |
| ++num_completed_worklets; |
| if (num_completed_worklets == kNumWorklets) |
| run_loop.Quit(); |
| })); |
| } |
| run_loop.Run(); |
| } |
| |
| // Test the case of a bunch of ScoreAd() calls in parallel, all started before |
| // the worklet script fails to load. |
| TEST_F(SellerWorkletTest, ScoreAdParallelLoadFails) { |
| auto seller_worklet = CreateWorklet(); |
| |
| for (size_t i = 0; i < 10; ++i) { |
| browser_signal_render_url_ = GURL(base::StringPrintf("https://foo/%zu", i)); |
| RunScoreAdOnWorkletExpectingCallbackNeverInvoked(seller_worklet.get()); |
| } |
| |
| // No calls should complete, since the script hasn't loaded yet. |
| task_environment_.RunUntilIdle(); |
| |
| // Script fails to load. |
| url_loader_factory_.AddResponse(decision_logic_url_.spec(), |
| /*content=*/std::string(), |
| net::HTTP_NOT_FOUND); |
| |
| // The worklet should fail to load. |
| EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.", |
| WaitForDisconnect()); |
| // The worklet script callbacks should not be invoked. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| // Test the case of a bunch of ScoreAd() calls in parallel, in the case trusted |
| // scoring signals is non-null. In this case, call AllBidsGenerated() between |
| // scoring each bid, which should result in requests being sent individually. |
| TEST_F(SellerWorkletTest, ScoreAdParallelTrustedScoringSignalsNotBatched) { |
| base::Time start_time = base::Time::Now(); |
| |
| // Seller script that gets the score from the `trustedScoringSignals` value of |
| // the passed in `renderUrl`. |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript( |
| "trustedScoringSignals.renderUrl[browserSignals.renderUrl]")); |
| trusted_scoring_signals_url_ = |
| GURL("https://url.test/trusted_scoring_signals"); |
| auto seller_worklet = CreateWorklet(); |
| |
| // Start scoring a bunch of worklets. Don't provide JSON responses, to make |
| // sure they all reside in the worklet's task list at once. |
| const size_t kNumWorklets = 10; |
| size_t num_completed_worklets = 0; |
| base::RunLoop run_loop; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| browser_signal_render_url_ = GURL(base::StringPrintf("https://foo/%zu", i)); |
| RunScoreAdOnWorkletAsync(seller_worklet.get(), /*expected_score=*/2 * i, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/i, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| base::BindLambdaForTesting([&]() { |
| ++num_completed_worklets; |
| if (num_completed_worklets == kNumWorklets) |
| run_loop.Quit(); |
| })); |
| seller_worklet->SendPendingSignalsRequests(); |
| } |
| |
| // Spin run loop so all requests reach the scoring worklet. |
| run_loop.RunUntilIdle(); |
| EXPECT_EQ(0u, num_completed_worklets); |
| |
| // Provide all JSON responses. |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| GURL trusted_scoring_signals = GURL(base::StringPrintf( |
| "%s?hostname=%s&renderUrls=https%%3A%%2F%%2Ffoo%%2F%zu", |
| trusted_scoring_signals_url_->spec().c_str(), |
| top_window_origin_.host().c_str(), i)); |
| std::string response_body = base::StringPrintf( |
| R"({"renderUrls": {"https://foo/%zu": %zu}})", i, 2 * i); |
| AddVersionedJsonResponse(&url_loader_factory_, trusted_scoring_signals, |
| response_body, /*data_version=*/i); |
| } |
| run_loop.Run(); |
| |
| // No time should have passed during this test, since the |
| // SendPendingSignalsRequests() calls ensure requests are send immediately, |
| // without waiting on a timer. Using a mock time ensures that the passage of |
| // wall clock time doesn't impact the current time, only delayed tasks and |
| // timers do. |
| EXPECT_EQ(base::Time::Now(), start_time); |
| } |
| |
| // Test the case of a bunch of ScoreAd() calls in parallel, in the case trusted |
| // scoring signals is non-null. In this case, don't call AllBidsGenerated() |
| // between scoring each bid, which should result in all requests being sent as a |
| // single request. |
| // |
| // In this test, the ordering is: |
| // 1) The worklet script load completes. |
| // 2) ScoreAd() calls are made. |
| // 3) The trusted bidding signals are loaded. |
| TEST_F(SellerWorkletTest, ScoreAdParallelTrustedScoringSignalsBatched1) { |
| // Seller script that gets the score from the `trustedScoringSignals` value of |
| // the passed in `renderUrl`. |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript( |
| "trustedScoringSignals.renderUrl[browserSignals.renderUrl]")); |
| trusted_scoring_signals_url_ = |
| GURL("https://url.test/trusted_scoring_signals"); |
| auto seller_worklet = CreateWorklet(); |
| |
| // Start scoring a bunch of worklets. Don't provide JSON responses, to make |
| // sure they all reside in the worklet's task list at once. |
| const size_t kNumWorklets = 10; |
| size_t num_completed_worklets = 0; |
| base::RunLoop run_loop; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| browser_signal_render_url_ = GURL(base::StringPrintf("https://foo/%zu", i)); |
| RunScoreAdOnWorkletAsync(seller_worklet.get(), /*expected_score=*/2 * i, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| base::BindLambdaForTesting([&]() { |
| ++num_completed_worklets; |
| if (num_completed_worklets == kNumWorklets) |
| run_loop.Quit(); |
| })); |
| } |
| |
| // Spin run loop so all requests reach the scoring worklet. |
| run_loop.RunUntilIdle(); |
| EXPECT_EQ(0u, num_completed_worklets); |
| |
| // Provide a single response for the merged URL request. |
| std::string request_url = |
| base::StringPrintf("%s?hostname=%s&renderUrls=", |
| trusted_scoring_signals_url_->spec().c_str(), |
| top_window_origin_.host().c_str()); |
| std::string response_body; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| if (i > 0) { |
| request_url += ","; |
| response_body += ","; |
| } |
| request_url += base::StringPrintf("https%%3A%%2F%%2Ffoo%%2F%zu", i); |
| response_body += base::StringPrintf(R"("https://foo/%zu": %zu)", i, 2 * i); |
| } |
| response_body = |
| base::StringPrintf(R"({"renderUrls": {%s}})", response_body.c_str()); |
| AddJsonResponse(&url_loader_factory_, GURL(request_url), response_body); |
| |
| // All ScoreAd() calls should succeed with the expected scores. |
| run_loop.Run(); |
| } |
| |
| // Same as above, but with different ordering. |
| // |
| // In this test, the ordering is: |
| // 1) ScoreAd() calls are made. |
| // 2) The worklet script load completes. |
| // 3) The trusted bidding signals are loaded. |
| TEST_F(SellerWorkletTest, ScoreAdParallelTrustedScoringSignalsBatched2) { |
| trusted_scoring_signals_url_ = |
| GURL("https://url.test/trusted_scoring_signals"); |
| auto seller_worklet = CreateWorklet(); |
| |
| // Start scoring a bunch of worklets. Don't provide JSON responses, to make |
| // sure they all reside in the worklet's task list at once. |
| const size_t kNumWorklets = 10; |
| size_t num_completed_worklets = 0; |
| base::RunLoop run_loop; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| browser_signal_render_url_ = GURL(base::StringPrintf("https://foo/%zu", i)); |
| RunScoreAdOnWorkletAsync(seller_worklet.get(), /*expected_score=*/2 * i, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/10, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| base::BindLambdaForTesting([&]() { |
| ++num_completed_worklets; |
| if (num_completed_worklets == kNumWorklets) |
| run_loop.Quit(); |
| })); |
| } |
| |
| // Spin run loop so all requests reach the scoring worklet. |
| run_loop.RunUntilIdle(); |
| EXPECT_EQ(0u, num_completed_worklets); |
| |
| // Return seller script that gets the score from the `trustedScoringSignals` |
| // value of the passed in `renderUrl`, and wait for it to finish loading. |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript( |
| "trustedScoringSignals.renderUrl[browserSignals.renderUrl]")); |
| task_environment_.RunUntilIdle(); |
| |
| // Provide a single response for the merged URL request. |
| std::string request_url = |
| base::StringPrintf("%s?hostname=%s&renderUrls=", |
| trusted_scoring_signals_url_->spec().c_str(), |
| top_window_origin_.host().c_str()); |
| std::string response_body; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| if (i > 0) { |
| request_url += ","; |
| response_body += ","; |
| } |
| request_url += base::StringPrintf("https%%3A%%2F%%2Ffoo%%2F%zu", i); |
| response_body += base::StringPrintf(R"("https://foo/%zu": %zu)", i, 2 * i); |
| } |
| response_body = |
| base::StringPrintf(R"({"renderUrls": {%s}})", response_body.c_str()); |
| AddVersionedJsonResponse(&url_loader_factory_, GURL(request_url), |
| response_body, /*data_version=*/10); |
| |
| // All ScoreAd() calls should succeed with the expected scores. |
| run_loop.Run(); |
| } |
| |
| // Same as above, but with different ordering. |
| // |
| // In this test, the ordering is: |
| // 1) ScoreAd() calls are made. |
| // 2) The trusted bidding signals are loaded. |
| // 3) The worklet script load completes. |
| TEST_F(SellerWorkletTest, ScoreAdParallelTrustedScoringSignalsBatched3) { |
| trusted_scoring_signals_url_ = |
| GURL("https://url.test/trusted_scoring_signals"); |
| auto seller_worklet = CreateWorklet(); |
| |
| // Start scoring a bunch of worklets. Don't provide JSON responses, to make |
| // sure they all reside in the worklet's task list at once. |
| const size_t kNumWorklets = 10; |
| size_t num_completed_worklets = 0; |
| base::RunLoop run_loop; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| browser_signal_render_url_ = GURL(base::StringPrintf("https://foo/%zu", i)); |
| RunScoreAdOnWorkletAsync(seller_worklet.get(), /*expected_score=*/2 * i, |
| /*expected_errors=*/std::vector<std::string>(), |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/10, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| base::BindLambdaForTesting([&]() { |
| ++num_completed_worklets; |
| if (num_completed_worklets == kNumWorklets) |
| run_loop.Quit(); |
| })); |
| } |
| |
| // Spin run loop so all requests reach the scoring worklet. |
| run_loop.RunUntilIdle(); |
| EXPECT_EQ(0u, num_completed_worklets); |
| |
| // Provide a single response for the merged URL request. |
| std::string request_url = |
| base::StringPrintf("%s?hostname=%s&renderUrls=", |
| trusted_scoring_signals_url_->spec().c_str(), |
| top_window_origin_.host().c_str()); |
| std::string response_body; |
| for (size_t i = 0; i < kNumWorklets; ++i) { |
| if (i > 0) { |
| request_url += ","; |
| response_body += ","; |
| } |
| request_url += base::StringPrintf("https%%3A%%2F%%2Ffoo%%2F%zu", i); |
| response_body += base::StringPrintf(R"("https://foo/%zu": %zu)", i, 2 * i); |
| } |
| response_body = |
| base::StringPrintf(R"({"renderUrls": {%s}})", response_body.c_str()); |
| AddVersionedJsonResponse(&url_loader_factory_, GURL(request_url), |
| response_body, /*data_version=*/10); |
| |
| // Spin run loop so the response is handled. No ScoreAdCalls should complete |
| // yet. |
| run_loop.RunUntilIdle(); |
| EXPECT_EQ(0u, num_completed_worklets); |
| |
| // Return seller script that gets the score from the `trustedScoringSignals` |
| // value of the passed in `renderUrl`, and wait for it to finish loading. |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript( |
| "trustedScoringSignals.renderUrl[browserSignals.renderUrl]")); |
| |
| // All ScoreAd() calls should succeed with the expected scores. |
| run_loop.Run(); |
| } |
| |
| // It shouldn't matter the order in which network fetches complete. For each |
| // required and optional scoreAd() URL load prerequisite, ensure that |
| // scoreAd() completes when that URL is the last loaded URL. |
| TEST_F(SellerWorkletTest, ScoreAdLoadCompletionOrder) { |
| constexpr char kJsonResponse[] = "{}"; |
| constexpr char kDirectFromSellerSignalsHeaders[] = |
| "X-Allow-FLEDGE: true\nX-FLEDGE-Auction-Only: true"; |
| |
| direct_from_seller_seller_signals_ = GURL("https://url.test/sellersignals"); |
| direct_from_seller_auction_signals_ = GURL("https://url.test/auctionsignals"); |
| trusted_scoring_signals_url_ = GURL("https://url.test/trustedsignals"); |
| |
| struct Response { |
| GURL response_url; |
| std::string response_type; |
| std::string headers; |
| std::string content; |
| }; |
| |
| const Response kResponses[] = { |
| {decision_logic_url_, kJavascriptMimeType, kAllowFledgeHeader, |
| CreateScoreAdScript("1")}, |
| {*direct_from_seller_seller_signals_, kJsonMimeType, |
| kDirectFromSellerSignalsHeaders, kJsonResponse}, |
| {*direct_from_seller_auction_signals_, kJsonMimeType, |
| kDirectFromSellerSignalsHeaders, kJsonResponse}, |
| {GURL(trusted_scoring_signals_url_->spec() + |
| "?hostname=window.test" |
| "&renderUrls=https%3A%2F%2Frender.url.test%2F"), |
| kJsonMimeType, kAllowFledgeHeader, kTrustedScoringSignalsResponse}}; |
| |
| // Cycle such that each response in `kResponses` gets to be the last response, |
| // like so: |
| // |
| // 0,1,2 |
| // 1,2,0 |
| // 2,0,1 |
| for (size_t offset = 0; offset < std::size(kResponses); ++offset) { |
| SCOPED_TRACE(offset); |
| mojo::Remote<mojom::SellerWorklet> seller_worklet = CreateWorklet(); |
| url_loader_factory_.ClearResponses(); |
| auto run_loop = std::make_unique<base::RunLoop>(); |
| RunScoreAdOnWorkletAsync(seller_worklet.get(), /*expected_score=*/1.0, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop->QuitClosure()); |
| for (size_t i = 0; i < std::size(kResponses); ++i) { |
| SCOPED_TRACE(i); |
| const Response& response = |
| kResponses[(i + offset) % std::size(kResponses)]; |
| AddResponse( |
| &url_loader_factory_, response.response_url, response.response_type, |
| /*charset=*/absl::nullopt, response.content, response.headers); |
| task_environment_.RunUntilIdle(); |
| if (i < std::size(kResponses) - 1) { |
| // Some URLs haven't finished loading -- generateBid() should be |
| // blocked. |
| EXPECT_FALSE(run_loop->AnyQuitCalled()); |
| } |
| } |
| // The last URL for this generateBid() call has completed -- check that |
| // generateBid() returns. |
| run_loop->Run(); |
| } |
| } |
| |
| // If multiple worklets request DirectFromSellerSignals, they each get the |
| // correct signals. |
| TEST_F(SellerWorkletTest, ScoreAdDirectFromSellerSignalsMultipleWorklets) { |
| constexpr char kWorklet1JsonResponse[] = R"({"worklet":1})"; |
| constexpr char kWorklet2JsonResponse[] = R"({"worklet":2})"; |
| constexpr char kWorklet1ExtraCode[] = R"( |
| const sellerSignalsJson = |
| JSON.stringify(directFromSellerSignals.sellerSignals); |
| if (sellerSignalsJson !== '{"worklet":1}') { |
| throw 'Wrong directFromSellerSignals.sellerSignals ' + |
| sellerSignalsJson; |
| } |
| const auctionSignalsJson = |
| JSON.stringify(directFromSellerSignals.auctionSignals); |
| if (auctionSignalsJson !== '{"worklet":1}') { |
| throw 'Wrong directFromSellerSignals.auctionSignals ' + |
| auctionSignalsJson; |
| } |
| )"; |
| constexpr char kWorklet2ExtraCode[] = R"( |
| const sellerSignalsJson = |
| JSON.stringify(directFromSellerSignals.sellerSignals); |
| if (sellerSignalsJson !== '{"worklet":2}') { |
| throw 'Wrong directFromSellerSignals.sellerSignals ' + |
| sellerSignalsJson; |
| } |
| const auctionSignalsJson = |
| JSON.stringify(directFromSellerSignals.auctionSignals); |
| if (auctionSignalsJson !== '{"worklet":2}') { |
| throw 'Wrong directFromSellerSignals.auctionSignals ' + |
| auctionSignalsJson; |
| } |
| )"; |
| constexpr char kDirectFromSellerSignalsHeaders[] = |
| "X-Allow-FLEDGE: true\nX-FLEDGE-Auction-Only: true"; |
| |
| direct_from_seller_seller_signals_ = GURL("https://url.test/sellersignals"); |
| direct_from_seller_auction_signals_ = GURL("https://url.test/auctionsignals"); |
| |
| mojo::Remote<mojom::SellerWorklet> seller_worklet1 = CreateWorklet(); |
| AddResponse(&url_loader_factory_, *direct_from_seller_seller_signals_, |
| kJsonMimeType, /*charset=*/absl::nullopt, kWorklet1JsonResponse, |
| kDirectFromSellerSignalsHeaders); |
| AddResponse(&url_loader_factory_, *direct_from_seller_auction_signals_, |
| kJsonMimeType, /*charset=*/absl::nullopt, kWorklet1JsonResponse, |
| kDirectFromSellerSignalsHeaders); |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript("1", /*extra_code=*/kWorklet1ExtraCode)); |
| |
| // For the second worklet, use a different `decision_logic_url_` (to set up |
| // different expectations), but use the same DirectFromSellerSignals URLs. |
| decision_logic_url_ = GURL("https://url2.test/"); |
| mojo::Remote<mojom::SellerWorklet> seller_worklet2 = CreateWorklet( |
| /*pause_for_debugger_on_start=*/false, |
| /*out_seller_worklet_impl=*/nullptr, |
| /*use_alternate_url_loader_factory=*/true); |
| AddResponse(&alternate_url_loader_factory_, |
| *direct_from_seller_seller_signals_, kJsonMimeType, |
| /*charset=*/absl::nullopt, kWorklet2JsonResponse, |
| kDirectFromSellerSignalsHeaders); |
| AddResponse(&alternate_url_loader_factory_, |
| *direct_from_seller_auction_signals_, kJsonMimeType, |
| /*charset=*/absl::nullopt, kWorklet2JsonResponse, |
| kDirectFromSellerSignalsHeaders); |
| AddJavascriptResponse( |
| &alternate_url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript("1", /*extra_code=*/kWorklet2ExtraCode)); |
| auto run_loop = std::make_unique<base::RunLoop>(); |
| RunScoreAdOnWorkletAsync(seller_worklet1.get(), /*expected_score=*/1.0, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop->QuitClosure()); |
| run_loop->Run(); |
| |
| run_loop = std::make_unique<base::RunLoop>(); |
| RunScoreAdOnWorkletAsync(seller_worklet2.get(), /*expected_score=*/1.0, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop->QuitClosure()); |
| run_loop->Run(); |
| } |
| |
| // 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(SellerWorkletTest, ReportResultParallel) { |
| auto seller_worklet = CreateWorklet(); |
| |
| // For the first loop iteration, call ReportResult() repeatedly before |
| // providing the seller script, then provide the seller script. For the second |
| // loop iteration, reuse the seller worklet from the first iteration, so the |
| // Javascript is loaded from the start. |
| for (bool report_result_invoked_before_worklet_script_loaded : |
| {false, true}) { |
| SCOPED_TRACE(report_result_invoked_before_worklet_script_loaded); |
| |
| base::RunLoop run_loop; |
| const size_t kNumReportResultCalls = 10; |
| size_t num_report_result_calls = 0; |
| for (size_t i = 0; i < kNumReportResultCalls; ++i) { |
| // Differentiate each call based on the bid. |
| bid_ = i + 1; |
| RunReportResultExpectingResultAsync( |
| seller_worklet.get(), |
| /*expected_signals_for_winner=*/base::NumberToString(bid_), |
| /*expected_report_url=*/ |
| GURL("https://" + base::NumberToString(bid_)), |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| /*expected_errors=*/{}, |
| base::BindLambdaForTesting([&run_loop, &num_report_result_calls]() { |
| ++num_report_result_calls; |
| if (num_report_result_calls == kNumReportResultCalls) |
| 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_result_invoked_before_worklet_script_loaded == false) { |
| task_environment_.RunUntilIdle(); |
| EXPECT_FALSE(run_loop.AnyQuitCalled()); |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateReportToScript( |
| /*raw_return_value=*/"browserSignals.bid", |
| /*extra_code=*/ |
| R"(sendReportTo("https://" + browserSignals.bid))")); |
| } |
| |
| run_loop.Run(); |
| EXPECT_EQ(kNumReportResultCalls, num_report_result_calls); |
| } |
| } |
| |
| // Test multiple ReportResult() calls on a single worklet, in parallel, in the |
| // case the worklet script fails to load. |
| TEST_F(SellerWorkletTest, ReportResultParallelLoadFails) { |
| auto seller_worklet = CreateWorklet(); |
| |
| for (size_t i = 0; i < 10; ++i) { |
| RunReportResultExpectingCallbackNeverInvoked(seller_worklet.get()); |
| } |
| |
| url_loader_factory_.AddResponse(decision_logic_url_.spec(), "Response body", |
| net::HTTP_NOT_FOUND); |
| |
| EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.", |
| WaitForDisconnect()); |
| } |
| |
| // Tests parsing of return values. |
| TEST_F(SellerWorkletTest, ReportResult) { |
| RunReportResultCreatedScriptExpectingResult( |
| "1", /*extra_code=*/std::string(), |
| /*expected_signals_for_winner=*/"1", |
| /*expected_report_url=*/absl::nullopt); |
| RunReportResultCreatedScriptExpectingResult( |
| R"(" 1 ")", /*extra_code=*/std::string(), |
| /*expected_signals_for_winner=*/R"(" 1 ")", |
| /*expected_report_url=*/absl::nullopt); |
| RunReportResultCreatedScriptExpectingResult( |
| "[ null ]", /*extra_code=*/std::string(), "[null]", |
| /*expected_report_url=*/absl::nullopt); |
| |
| // No return value. |
| RunReportResultCreatedScriptExpectingResult( |
| "", /*extra_code=*/std::string(), "null", |
| /*expected_report_url=*/absl::nullopt); |
| |
| // Throw exception. |
| RunReportResultCreatedScriptExpectingResult( |
| "shrimp", /*extra_code=*/std::string(), |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:11 Uncaught ReferenceError: " |
| "shrimp is not defined."}); |
| } |
| |
| // Tests reporting URLs. |
| TEST_F(SellerWorkletTest, ReportResultSendReportTo) { |
| RunReportResultCreatedScriptExpectingResult( |
| "1", R"(sendReportTo("https://foo.test"))", |
| /*expected_signals_for_winner=*/"1", GURL("https://foo.test/")); |
| RunReportResultCreatedScriptExpectingResult( |
| "1", R"(sendReportTo("https://foo.test/bar"))", |
| /*expected_signals_for_winner=*/"1", GURL("https://foo.test/bar")); |
| |
| // Disallowed schemes. |
| RunReportResultCreatedScriptExpectingResult( |
| "1", R"(sendReportTo("http://foo.test/"))", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: " |
| "sendReportTo must be passed a valid HTTPS url."}); |
| RunReportResultCreatedScriptExpectingResult( |
| "1", R"(sendReportTo("file:///foo/"))", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: " |
| "sendReportTo must be passed a valid HTTPS url."}); |
| |
| // Multiple calls. |
| RunReportResultCreatedScriptExpectingResult( |
| "1", |
| R"(sendReportTo("https://foo.test/"); sendReportTo("https://foo.test/"))", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: " |
| "sendReportTo may be called at most once."}); |
| |
| // No message if caught, but still no URL. |
| RunReportResultCreatedScriptExpectingResult( |
| "1", |
| R"(try { |
| sendReportTo("https://foo.test/"); |
| sendReportTo("https://foo.test/")} catch(e) {})", |
| /*expected_signals_for_winner=*/"1", |
| /*expected_report_url=*/absl::nullopt); |
| |
| // Not a URL. |
| RunReportResultCreatedScriptExpectingResult( |
| "1", R"(sendReportTo("France"))", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: " |
| "sendReportTo must be passed a valid HTTPS url."}); |
| RunReportResultCreatedScriptExpectingResult( |
| "1", R"(sendReportTo(null))", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: " |
| "sendReportTo requires 1 string parameter."}); |
| RunReportResultCreatedScriptExpectingResult( |
| "1", R"(sendReportTo([5]))", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: " |
| "sendReportTo requires 1 string parameter."}); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultDateNotAvailable) { |
| RunReportResultCreatedScriptExpectingResult( |
| "1", R"(sendReportTo("https://foo.test/" + Date().toString()))", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught ReferenceError: Date is not defined."}); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultTopWindowOrigin) { |
| top_window_origin_ = url::Origin::Create(GURL("https://foo.test/")); |
| RunReportResultCreatedScriptExpectingResult( |
| R"(browserSignals.topWindowHostname == "foo.test" ? 2 : 1)", |
| /*extra_code=*/std::string(), "2", |
| /*expected_report_url=*/absl::nullopt); |
| |
| top_window_origin_ = url::Origin::Create(GURL("https://[::1]:40000/")); |
| RunReportResultCreatedScriptExpectingResult( |
| R"(browserSignals.topWindowHostname == "[::1]" ? 3 : 1)", |
| /*extra_code=*/std::string(), "3", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultTopLevelSeller) { |
| browser_signals_other_seller_.reset(); |
| RunReportResultCreatedScriptExpectingResult( |
| R"("topLevelSeller" in browserSignals ? 0 : 1)", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"1", |
| /*expected_report_url=*/absl::nullopt); |
| |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.seller.test"))); |
| browser_signals_component_auction_report_result_params_ = |
| mojom::ComponentAuctionReportResultParams::New( |
| /*top_level_seller_signals=*/"null", |
| /*modified_bid=*/0, |
| /*has_modified_bid=*/false); |
| RunReportResultCreatedScriptExpectingResult( |
| R"(browserSignals.topLevelSeller === "https://top.seller.test" ? 2 : 0)", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"2", |
| /*expected_report_url=*/absl::nullopt); |
| browser_signals_component_auction_report_result_params_.reset(); |
| |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.test"))); |
| RunReportResultCreatedScriptExpectingResult( |
| R"("topLevelSeller" in browserSignals ? 0 : 3)", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"3", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultComponentSeller) { |
| browser_signals_other_seller_.reset(); |
| RunReportResultCreatedScriptExpectingResult( |
| R"("componentSeller" in browserSignals ? 0 : 1)", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"1", |
| /*expected_report_url=*/absl::nullopt); |
| |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.seller.test"))); |
| browser_signals_component_auction_report_result_params_ = |
| mojom::ComponentAuctionReportResultParams::New( |
| /*top_level_seller_signals=*/"null", |
| /*modified_bid=*/0, |
| /*has_modified_bid=*/false); |
| RunReportResultCreatedScriptExpectingResult( |
| R"("componentSeller" in browserSignals ? 0 : 2)", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"2", |
| /*expected_report_url=*/absl::nullopt); |
| browser_signals_component_auction_report_result_params_.reset(); |
| |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.test"))); |
| RunReportResultCreatedScriptExpectingResult( |
| R"(browserSignals.componentSeller === "https://component.test" ? 3 : 0)", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"3", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultTopLevelSellerSignals) { |
| // Top-level auctions should never be passed a `topLevelSellerSignals` field, |
| // whether ReportResult() is invoked with a bid from a component auction or |
| // the top-level auction. |
| |
| browser_signals_other_seller_.reset(); |
| RunReportResultCreatedScriptExpectingResult( |
| "'topLevelSellerSignals' in browserSignals ? 0 : 1", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"1", |
| /*expected_report_url=*/absl::nullopt); |
| |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.test"))); |
| RunReportResultCreatedScriptExpectingResult( |
| "'topLevelSellerSignals' in browserSignals ? 0 : 2", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"2", |
| /*expected_report_url=*/absl::nullopt); |
| |
| // Component auctions should take `topLevelSellerSignals` from the |
| // ComponentAuctionReportResultParams argument. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.seller.test"))); |
| browser_signals_component_auction_report_result_params_ = |
| mojom::ComponentAuctionReportResultParams::New( |
| /*top_level_seller_signals=*/"null", |
| /*modified_bid=*/0, |
| /*has_modified_bid=*/false); |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.topLevelSellerSignals === null ? 3 : 0", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"3", |
| /*expected_report_url=*/absl::nullopt); |
| |
| browser_signals_component_auction_report_result_params_ |
| ->top_level_seller_signals = "[4]"; |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.topLevelSellerSignals[0]", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"4", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultModifiedBid) { |
| // Top-level auctions should never be passed a `modifiedBid` field, whether |
| // ReportResult() is invoked with a bit from a component auction or the |
| // top-level auction. |
| |
| browser_signals_other_seller_.reset(); |
| RunReportResultCreatedScriptExpectingResult( |
| "'modifiedBid' in browserSignals ? 0 : 1", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"1", |
| /*expected_report_url=*/absl::nullopt); |
| |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewComponentSeller( |
| url::Origin::Create(GURL("https://component.test"))); |
| RunReportResultCreatedScriptExpectingResult( |
| "'modifiedBid' in browserSignals ? 0 : 2", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"2", |
| /*expected_report_url=*/absl::nullopt); |
| |
| // Component auctions should only receive a `modifiedBid` field when |
| // `has_modified_bid` is true. |
| browser_signals_other_seller_ = |
| mojom::ComponentAuctionOtherSeller::NewTopLevelSeller( |
| url::Origin::Create(GURL("https://top.seller.test"))); |
| browser_signals_component_auction_report_result_params_ = |
| mojom::ComponentAuctionReportResultParams::New( |
| /*top_level_seller_signals=*/"null", |
| /*modified_bid=*/4, |
| /*has_modified_bid=*/true); |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.modifiedBid", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"4", |
| /*expected_report_url=*/absl::nullopt); |
| browser_signals_component_auction_report_result_params_->has_modified_bid = |
| false; |
| RunReportResultCreatedScriptExpectingResult( |
| "'modifiedBid' in browserSignals ? 0 : 3", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"3", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultInterestGroupOwner) { |
| browser_signal_interest_group_owner_ = |
| url::Origin::Create(GURL("https://foo.test/")); |
| RunReportResultCreatedScriptExpectingResult( |
| R"(browserSignals.interestGroupOwner == "https://foo.test" ? 2 : 1)", |
| /*extra_code=*/std::string(), "2", |
| /*expected_report_url=*/absl::nullopt); |
| |
| browser_signal_interest_group_owner_ = |
| url::Origin::Create(GURL("https://[::1]:40000/")); |
| RunReportResultCreatedScriptExpectingResult( |
| R"(browserSignals.interestGroupOwner == "https://[::1]:40000" ? 3 : 1)", |
| /*extra_code=*/std::string(), "3", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultBuyerAndSellerReportingId) { |
| browser_signal_buyer_and_seller_reporting_id_ = "campaign"; |
| RunReportResultCreatedScriptExpectingResult( |
| R"(browserSignals.buyerAndSellerReportingId === "campaign" ? 2 : 1)", |
| /*extra_code=*/std::string(), "2", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultRenderUrl) { |
| browser_signal_render_url_ = GURL("https://foo/"); |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.renderUrl", "sendReportTo(browserSignals.renderUrl)", |
| R"("https://foo/")", browser_signal_render_url_); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultRegisterAdBeacon) { |
| bid_ = 5; |
| base::flat_map<std::string, GURL> expected_ad_beacon_map = { |
| {"click", GURL("https://click.example.com/")}, |
| {"view", GURL("https://view.example.com/")}, |
| }; |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(registerAdBeacon({ |
| 'click': "https://click.example.com/", |
| 'view': "https://view.example.com/", |
| }))", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url =*/absl::nullopt, expected_ad_beacon_map); |
| |
| browser_signal_render_url_ = GURL("https://foo/"); |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(registerAdBeacon({ |
| 'click': "https://click.example.com/", |
| 'view': "https://view.example.com/", |
| }); |
| sendReportTo(browserSignals.renderUrl))", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url =*/browser_signal_render_url_, |
| expected_ad_beacon_map); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(sendReportTo(browserSignals.renderUrl); |
| registerAdBeacon({ |
| 'click': "https://click.example.com/", |
| 'view': "https://view.example.com/", |
| }))", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url =*/browser_signal_render_url_, |
| expected_ad_beacon_map); |
| |
| // Don't call twice. |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(registerAdBeacon({ |
| 'click': "https://click.example.com/", |
| 'view': "https://view.example.com/", |
| }); |
| registerAdBeacon())", |
| /*expected_signals_for_winner=*/{}, |
| /*expected_report_url =*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:14 Uncaught TypeError: registerAdBeacon may be " |
| "called at most once."}); |
| |
| // If called twice and the error is caught, use the first result. |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(registerAdBeacon({ |
| 'click': "https://click.example.com/", |
| 'view': "https://view.example.com/", |
| }); |
| try { registerAdBeacon() } |
| catch (e) {})", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url =*/absl::nullopt, expected_ad_beacon_map); |
| |
| // If error on first call, can be called again. |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(try { registerAdBeacon() } |
| catch (e) {} |
| registerAdBeacon({ |
| 'click': "https://click.example.com/", |
| 'view': "https://view.example.com/", |
| }))", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url =*/absl::nullopt, expected_ad_beacon_map); |
| |
| // Error if no parameters |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", R"(registerAdBeacon())", |
| /*expected_signals_for_winner=*/{}, |
| /*expected_report_url =*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: registerAdBeacon requires 1 " |
| "object parameter."}); |
| |
| // Error if parameter is not an object |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", R"(registerAdBeacon("foo"))", |
| /*expected_signals_for_winner=*/{}, |
| /*expected_report_url =*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: registerAdBeacon requires 1 " |
| "object parameter."}); |
| |
| // Error if parameter is not an object |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", R"(registerAdBeacon("foo"))", |
| /*expected_signals_for_winner=*/{}, |
| /*expected_report_url =*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: registerAdBeacon requires 1 " |
| "object parameter."}); |
| |
| // Error if parameter attributes are not strings |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(registerAdBeacon({ |
| 'click': "https://click.example.com/", |
| 1: "https://view.example.com/", |
| }))", |
| /*expected_signals_for_winner=*/{}, |
| /*expected_report_url =*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: registerAdBeacon object " |
| "attributes must be strings."}); |
| |
| // Error if invalid reporting URL |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(registerAdBeacon({ |
| 'click': "https://click.example.com/", |
| 'view': "gopher://view.example.com/", |
| }))", |
| /*expected_signals_for_winner=*/{}, |
| /*expected_report_url =*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: registerAdBeacon invalid " |
| "reporting url for key 'view': 'gopher://view.example.com/'."}); |
| |
| // Error if not trustworthy reporting URL |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(registerAdBeacon({ |
| 'click': "https://127.0.0.1/", |
| 'view': "http://view.example.com/", |
| }))", |
| /*expected_signals_for_winner=*/{}, |
| /*expected_report_url =*/absl::nullopt, |
| /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| {"https://url.test/:10 Uncaught TypeError: registerAdBeacon invalid " |
| "reporting url for key 'view': 'http://view.example.com/'."}); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultBid) { |
| bid_ = 5; |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.bid + typeof browserSignals.bid", |
| /*extra_code=*/std::string(), R"("5number")", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultBidCurrency) { |
| bid_currency_ = blink::AdCurrency::From("EUR"); |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.bidCurrency + typeof browserSignals.bidCurrency", |
| /*extra_code=*/std::string(), R"("EURstring")", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultDesireability) { |
| browser_signal_desireability_ = 10; |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.desirability + typeof browserSignals.desirability", |
| /*extra_code=*/std::string(), R"("10number")", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultHighestScoringOtherBid) { |
| browser_signal_highest_scoring_other_bid_ = 5; |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.highestScoringOtherBid + typeof " |
| "browserSignals.highestScoringOtherBid", |
| /*extra_code=*/std::string(), R"("5number")", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultHighestScoringOtherBidCurrency) { |
| browser_signal_highest_scoring_other_bid_currency_ = |
| blink::AdCurrency::From("EUR"); |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.highestScoringOtherBidCurrency + typeof " |
| "browserSignals.highestScoringOtherBidCurrency", |
| /*extra_code=*/std::string(), R"("EURstring")", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultAuctionConfigParam) { |
| // Empty AuctionAdConfig, with nothing filled in, except the seller and |
| // decision logic URL. |
| decision_logic_url_ = GURL("https://example.com/auction.js"); |
| RunReportResultCreatedScriptExpectingResult( |
| "auctionConfig", /*extra_code=*/std::string(), |
| R"({"seller":"https://example.com",)" |
| R"("decisionLogicUrl":"https://example.com/auction.js"})", |
| /*expected_report_url=*/absl::nullopt); |
| |
| // Everything filled in but component auctions (can't include component |
| // auctions and non-empty interestGroupBuyers, so test those cases |
| // separately). |
| decision_logic_url_ = GURL("https://example.com/auction.js"); |
| trusted_scoring_signals_url_ = |
| GURL("https://example.com/scoring_signals.json"); |
| auction_ad_config_non_shared_params_.interest_group_buyers = { |
| url::Origin::Create(GURL("https://buyer1.com")), |
| url::Origin::Create(GURL("https://another-buyer.com"))}; |
| auction_ad_config_non_shared_params_.auction_signals = |
| blink::AuctionConfig::MaybePromiseJson::FromValue( |
| R"({"is_auction_signals": true})"); |
| auction_ad_config_non_shared_params_.seller_signals = |
| blink::AuctionConfig::MaybePromiseJson::FromValue( |
| R"({"is_seller_signals": true})"); |
| auction_ad_config_non_shared_params_.seller_timeout = base::Milliseconds(200); |
| base::flat_map<url::Origin, std::string> per_buyer_signals; |
| per_buyer_signals[url::Origin::Create(GURL("https://a.com"))] = |
| R"({"signals_a": "A"})"; |
| per_buyer_signals[url::Origin::Create(GURL("https://b.com"))] = |
| R"({"signals_b": "B"})"; |
| auction_ad_config_non_shared_params_.per_buyer_signals = |
| blink::AuctionConfig::MaybePromisePerBuyerSignals::FromValue( |
| std::move(per_buyer_signals)); |
| |
| blink::AuctionConfig::BuyerTimeouts buyer_timeouts; |
| buyer_timeouts.per_buyer_timeouts.emplace(); |
| buyer_timeouts.per_buyer_timeouts |
| .value()[url::Origin::Create(GURL("https://a.com"))] = |
| base::Milliseconds(100); |
| buyer_timeouts.all_buyers_timeout = base::Milliseconds(150); |
| auction_ad_config_non_shared_params_.buyer_timeouts = |
| blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromValue( |
| std::move(buyer_timeouts)); |
| |
| blink::AuctionConfig::BuyerTimeouts buyer_cumulative_timeouts; |
| buyer_cumulative_timeouts.per_buyer_timeouts.emplace(); |
| buyer_cumulative_timeouts.per_buyer_timeouts |
| .value()[url::Origin::Create(GURL("https://a.com"))] = |
| base::Milliseconds(101); |
| buyer_cumulative_timeouts.all_buyers_timeout = base::Milliseconds(151); |
| auction_ad_config_non_shared_params_.buyer_cumulative_timeouts = |
| blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromValue( |
| std::move(buyer_cumulative_timeouts)); |
| |
| blink::AuctionConfig::BuyerCurrencies buyer_currencies; |
| buyer_currencies.per_buyer_currencies.emplace(); |
| buyer_currencies.per_buyer_currencies |
| .value()[url::Origin::Create(GURL("https://example.ca"))] = |
| blink::AdCurrency::From("CAD"); |
| buyer_currencies.all_buyers_currency = blink::AdCurrency::From("USD"); |
| auction_ad_config_non_shared_params_.buyer_currencies = |
| blink::AuctionConfig::MaybePromiseBuyerCurrencies::FromValue( |
| std::move(buyer_currencies)); |
| |
| auction_ad_config_non_shared_params_.per_buyer_priority_signals = { |
| {url::Origin::Create(GURL("https://a.com")), {{"signals_c", 0.5}}}}; |
| auction_ad_config_non_shared_params_.all_buyers_priority_signals = { |
| {"signals_d", 0}}; |
| |
| const char kExpectedJson1[] = |
| R"({"seller":"https://example.com", |
| "decisionLogicUrl":"https://example.com/auction.js", |
| "trustedScoringSignalsUrl":"https://example.com/scoring_signals.json", |
| "interestGroupBuyers":["https://buyer1.com", |
| "https://another-buyer.com"], |
| "auctionSignals":{"is_auction_signals":true}, |
| "sellerSignals":{"is_seller_signals":true}, |
| "sellerTimeout":200, |
| "perBuyerSignals":{"https://a.com":{"signals_a":"A"}, |
| "https://b.com":{"signals_b":"B"}}, |
| "perBuyerCurrencies":{"*": "USD", |
| "https://example.ca": "CAD"}, |
| "perBuyerTimeouts":{"https://a.com":100,"*":150}, |
| "perBuyerCumulativeTimeouts":{"https://a.com":101,"*":151}, |
| "perBuyerPrioritySignals":{"https://a.com":{"signals_c":0.5}, |
| "*": {"signals_d":0}} |
| })"; |
| RunReportResultCreatedScriptExpectingResult( |
| "auctionConfig", /*extra_code=*/std::string(), kExpectedJson1, |
| /*expected_report_url=*/absl::nullopt); |
| |
| // Clear NonSharedParams(), and add and populate two component auctions, each |
| // with one the mandatory `seller` and `decision_logic_url` fields filled in, |
| // and one extra field: One that's directly a member of the AuctionAdConfig, |
| // and one that's in the non-shared params. |
| auction_ad_config_non_shared_params_ = |
| blink::AuctionConfig::NonSharedParams(); |
| auto& component_auctions = |
| auction_ad_config_non_shared_params_.component_auctions; |
| |
| component_auctions.emplace_back(blink::AuctionConfig()); |
| component_auctions[0].seller = |
| url::Origin::Create(GURL("https://component1.com")); |
| component_auctions[0].decision_logic_url = |
| GURL("https://component1.com/script.js"); |
| component_auctions[0].non_shared_params.seller_timeout = |
| base::Milliseconds(111); |
| |
| component_auctions.emplace_back(blink::AuctionConfig()); |
| component_auctions[1].seller = |
| url::Origin::Create(GURL("https://component2.com")); |
| component_auctions[1].decision_logic_url = |
| GURL("https://component2.com/script.js"); |
| component_auctions[1].trusted_scoring_signals_url = |
| GURL("https://component2.com/signals.json"); |
| |
| const char kExpectedJson2[] = |
| R"({"seller":"https://example.com", |
| "decisionLogicUrl":"https://example.com/auction.js", |
| "trustedScoringSignalsUrl":"https://example.com/scoring_signals.json", |
| "componentAuctions":[ |
| {"seller":"https://component1.com", |
| "decisionLogicUrl":"https://component1.com/script.js", |
| "sellerTimeout":111}, |
| {"seller":"https://component2.com", |
| "decisionLogicUrl":"https://component2.com/script.js", |
| "trustedScoringSignalsUrl":"https://component2.com/signals.json"} |
| ]})"; |
| RunReportResultCreatedScriptExpectingResult( |
| "auctionConfig", /*extra_code=*/std::string(), kExpectedJson2, |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultAuctionConfigParamPerBuyerTimeouts) { |
| // Empty AuctionAdConfig, with nothing filled in, except the seller and |
| // decision logic URL. |
| decision_logic_url_ = GURL("https://example.com/auction.js"); |
| RunReportResultCreatedScriptExpectingResult( |
| "auctionConfig", /*extra_code=*/std::string(), |
| R"({"seller":"https://example.com",)" |
| R"("decisionLogicUrl":"https://example.com/auction.js"})", |
| /*expected_report_url=*/absl::nullopt); |
| |
| { |
| blink::AuctionConfig::BuyerTimeouts buyer_timeouts; |
| buyer_timeouts.per_buyer_timeouts.emplace(); |
| auction_ad_config_non_shared_params_.buyer_timeouts = |
| blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromValue( |
| std::move(buyer_timeouts)); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| "auctionConfig", /*extra_code=*/std::string(), |
| R"({"seller":"https://example.com",)" |
| R"("decisionLogicUrl":"https://example.com/auction.js",)" |
| R"("perBuyerTimeouts":{}})", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| { |
| blink::AuctionConfig::BuyerTimeouts buyer_timeouts; |
| buyer_timeouts.per_buyer_timeouts.emplace(); |
| buyer_timeouts.all_buyers_timeout = base::Milliseconds(150); |
| auction_ad_config_non_shared_params_.buyer_timeouts = |
| blink::AuctionConfig::MaybePromiseBuyerTimeouts::FromValue( |
| std::move(buyer_timeouts)); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| "auctionConfig", /*extra_code=*/std::string(), |
| R"({"seller":"https://example.com",)" |
| R"("decisionLogicUrl":"https://example.com/auction.js",)" |
| R"("perBuyerTimeouts":{"*":150}})", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultExperimentGroupIdParam) { |
| RunReportResultCreatedScriptExpectingResult( |
| R"("experimentGroupId" in auctionConfig ? 1 : 0)", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"0", |
| /*expected_report_url=*/absl::nullopt); |
| |
| experiment_group_id_ = 954u; |
| RunReportResultCreatedScriptExpectingResult( |
| "auctionConfig.experimentGroupId", |
| /*extra_code=*/std::string(), /*expected_signals_for_winner=*/"954", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| TEST_F(SellerWorkletTest, ReportResultDataVersion) { |
| browser_signal_data_version_ = 20; |
| RunReportResultCreatedScriptExpectingResult( |
| "browserSignals.dataVersion", /*extra_code=*/std::string(), |
| /*expected_signals_for_winner=*/"20", |
| /*expected_report_url=*/absl::nullopt); |
| } |
| |
| // It shouldn't matter the order in which network fetches complete. For each |
| // required and optional reportResult() URL load prerequisite, ensure that |
| // reportResult() completes when that URL is the last loaded URL. |
| TEST_F(SellerWorkletTest, ReportResultLoadCompletionOrder) { |
| constexpr char kJsonResponse[] = "{}"; |
| constexpr char kDirectFromSellerSignalsHeaders[] = |
| "X-Allow-FLEDGE: true\nX-FLEDGE-Auction-Only: true"; |
| |
| direct_from_seller_seller_signals_ = GURL("https://url.test/sellersignals"); |
| direct_from_seller_auction_signals_ = GURL("https://url.test/auctionsignals"); |
| |
| struct Response { |
| GURL response_url; |
| std::string response_type; |
| std::string headers; |
| std::string content; |
| }; |
| |
| const Response kResponses[] = { |
| {decision_logic_url_, kJavascriptMimeType, kAllowFledgeHeader, |
| CreateReportToScript( |
| "1", |
| /*extra_code=*/R"(sendReportTo("https://foo.test"))")}, |
| {*direct_from_seller_seller_signals_, kJsonMimeType, |
| kDirectFromSellerSignalsHeaders, kJsonResponse}, |
| {*direct_from_seller_auction_signals_, kJsonMimeType, |
| kDirectFromSellerSignalsHeaders, kJsonResponse}}; |
| |
| // Cycle such that each response in `kResponses` gets to be the last response, |
| // like so: |
| // |
| // 0,1,2 |
| // 1,2,0 |
| // 2,0,1 |
| for (size_t offset = 0; offset < std::size(kResponses); ++offset) { |
| SCOPED_TRACE(offset); |
| mojo::Remote<mojom::SellerWorklet> seller_worklet = CreateWorklet(); |
| url_loader_factory_.ClearResponses(); |
| auto run_loop = std::make_unique<base::RunLoop>(); |
| RunReportResultExpectingResultAsync( |
| seller_worklet.get(), "1", GURL("https://foo.test/"), |
| /*expected_ad_beacon_map=*/{}, /*expected_pa_requests=*/{}, |
| /*expected_errors=*/{}, run_loop->QuitClosure()); |
| for (size_t i = 0; i < std::size(kResponses); ++i) { |
| SCOPED_TRACE(i); |
| const Response& response = |
| kResponses[(i + offset) % std::size(kResponses)]; |
| AddResponse( |
| &url_loader_factory_, response.response_url, response.response_type, |
| /*charset=*/absl::nullopt, response.content, response.headers); |
| task_environment_.RunUntilIdle(); |
| if (i < std::size(kResponses) - 1) { |
| // Some URLs haven't finished loading -- generateBid() should be |
| // blocked. |
| EXPECT_FALSE(run_loop->AnyQuitCalled()); |
| } |
| } |
| // The last URL for this generateBid() call has completed -- check that |
| // generateBid() returns. |
| run_loop->Run(); |
| } |
| } |
| |
| // Subsequent runs of the same script should not affect each other. Same is true |
| // for different scripts, but it follows from the single script case. |
| TEST_F(SellerWorkletTest, ScriptIsolation) { |
| // Use arrays so that all values are references, to catch both the case where |
| // variables are persisted, and the case where what they refer to is |
| // persisted, but variables are overwritten between runs. |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| R"( |
| // Globally scoped variable. |
| if (!globalThis.var1) |
| globalThis.var1 = [1]; |
| scoreAd = function() { |
| // Value only visible within this closure. |
| var var2 = [2]; |
| return function() { |
| if (2 == ++globalThis.var1[0] && 3 == ++var2[0]) |
| return 2; |
| return 1; |
| } |
| }(); |
| |
| reportResult = scoreAd; |
| )"); |
| auto seller_worklet = CreateWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| |
| for (int i = 0; i < 3; ++i) { |
| // Run each script twice in a row, to cover both cases where the same |
| // function is run sequentially, and when one function is run after the |
| // other. |
| for (int j = 0; j < 2; ++j) { |
| base::RunLoop run_loop; |
| seller_worklet->ScoreAd( |
| ad_metadata_, bid_, bid_currency_, |
| auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, |
| direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), component_expect_bid_currency_, |
| browser_signal_interest_group_owner_, browser_signal_render_url_, |
| browser_signal_ad_components_, browser_signal_bidding_duration_msecs_, |
| seller_timeout_, |
| /*trace_id=*/1, |
| TestScoreAdClient::Create(base::BindLambdaForTesting( |
| [&run_loop](double score, mojom::RejectReason reject_reason, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| component_auction_modified_bid_params, |
| absl::optional<double> bid_in_seller_currency, |
| absl::optional<uint32_t> scoring_signals_data_version, |
| const absl::optional<GURL>& debug_loss_report_url, |
| const absl::optional<GURL>& debug_win_report_url, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) { |
| EXPECT_EQ(2, score); |
| EXPECT_FALSE(scoring_signals_data_version.has_value()); |
| EXPECT_TRUE(errors.empty()); |
| run_loop.Quit(); |
| }))); |
| run_loop.Run(); |
| } |
| |
| for (int j = 0; j < 2; ++j) { |
| base::RunLoop run_loop; |
| seller_worklet->ReportResult( |
| auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, |
| direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), |
| browser_signal_interest_group_owner_, |
| browser_signal_buyer_and_seller_reporting_id_, |
| browser_signal_render_url_, bid_, bid_currency_, |
| browser_signal_desireability_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_highest_scoring_other_bid_currency_, |
| browser_signals_component_auction_report_result_params_.Clone(), |
| browser_signal_data_version_.value_or(0), |
| browser_signal_data_version_.has_value(), |
| /*trace_id=*/1, |
| base::BindLambdaForTesting( |
| [&run_loop]( |
| const absl::optional<std::string>& signals_for_winner, |
| const absl::optional<GURL>& report_url, |
| const base::flat_map<std::string, GURL>& ad_beacon_map, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) { |
| EXPECT_EQ("2", signals_for_winner); |
| EXPECT_TRUE(errors.empty()); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| } |
| } |
| |
| TEST_F(SellerWorkletTest, DeleteBeforeScoreAdCallback) { |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| CreateBasicSellAdScript()); |
| auto seller_worklet = CreateWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| |
| base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get()); |
| seller_worklet->ScoreAd( |
| ad_metadata_, bid_, bid_currency_, auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), component_expect_bid_currency_, |
| browser_signal_interest_group_owner_, browser_signal_render_url_, |
| browser_signal_ad_components_, browser_signal_bidding_duration_msecs_, |
| seller_timeout_, |
| /*trace_id=*/1, |
| TestScoreAdClient::Create( |
| // Callback should not be invoked since worklet deleted |
| TestScoreAdClient::ScoreAdNeverInvokedCallback())); |
| base::RunLoop().RunUntilIdle(); |
| seller_worklet.reset(); |
| event_handle->Signal(); |
| } |
| |
| TEST_F(SellerWorkletTest, DeleteBeforeReportResultCallback) { |
| AddJavascriptResponse( |
| &url_loader_factory_, decision_logic_url_, |
| CreateReportToScript("1", R"(sendReportTo("https://foo.test"))")); |
| auto seller_worklet = CreateWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| // Need to call ScoreAd() calling ReportResult(). |
| RunScoreAdExpectingResultOnWorklet(seller_worklet.get(), 1); |
| |
| base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get()); |
| seller_worklet->ReportResult( |
| auction_ad_config_non_shared_params_, direct_from_seller_seller_signals_, |
| direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), |
| browser_signal_interest_group_owner_, |
| browser_signal_buyer_and_seller_reporting_id_, browser_signal_render_url_, |
| bid_, bid_currency_, browser_signal_desireability_, |
| browser_signal_highest_scoring_other_bid_, |
| browser_signal_highest_scoring_other_bid_currency_, |
| browser_signals_component_auction_report_result_params_.Clone(), |
| browser_signal_data_version_.value_or(0), |
| browser_signal_data_version_.has_value(), |
| /*trace_id=*/1, |
| base::BindOnce([](const absl::optional<std::string>& signals_for_winner, |
| const absl::optional<GURL>& report_url, |
| const base::flat_map<std::string, GURL>& ad_beacon_map, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) { |
| ADD_FAILURE() << "Callback should not be invoked since worklet deleted"; |
| })); |
| base::RunLoop().RunUntilIdle(); |
| seller_worklet.reset(); |
| event_handle->Signal(); |
| } |
| |
| TEST_F(SellerWorkletTest, PauseOnStart) { |
| // If pause isn't working, this will be used and not the right script. |
| url_loader_factory_.AddResponse(decision_logic_url_.spec(), "", |
| net::HTTP_NOT_FOUND); |
| |
| SellerWorklet* worklet_impl = nullptr; |
| auto worklet = |
| CreateWorklet(/*pause_for_debugger_on_start=*/true, &worklet_impl); |
| // Grab the context ID to be able to resume. |
| int id = worklet_impl->context_group_id_for_testing(); |
| |
| // Queue a ScoreAd() call, which should not happen immediately since loading |
| // is paused. |
| base::RunLoop run_loop; |
| RunScoreAdOnWorkletAsync(worklet.get(), /*expected_score=*/10, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop.QuitClosure()); |
| |
| // Give it a chance to fetch. |
| task_environment_.RunUntilIdle(); |
| EXPECT_FALSE(run_loop.AnyQuitCalled()); |
| |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript("10")); |
| |
| task_environment_.RunUntilIdle(); |
| EXPECT_FALSE(run_loop.AnyQuitCalled()); |
| |
| // Let the ScoreAd() call run. |
| v8_helper_->v8_runner()->PostTask( |
| FROM_HERE, base::BindOnce([](scoped_refptr<AuctionV8Helper> v8_helper, |
| int id) { v8_helper->Resume(id); }, |
| v8_helper_, id)); |
| |
| run_loop.RunUntilIdle(); |
| } |
| |
| TEST_F(SellerWorkletTest, PauseOnStartDelete) { |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| CreateScoreAdScript("10")); |
| |
| SellerWorklet* worklet_impl = nullptr; |
| auto worklet = |
| CreateWorklet(/*pause_for_debugger_on_start=*/true, &worklet_impl); |
| |
| // Queue a ScoreAd() call, which should start paused and will never be run. |
| base::RunLoop run_loop; |
| RunScoreAdOnWorkletExpectingCallbackNeverInvoked(worklet.get()); |
| |
| // Give it a chance to fetch. |
| task_environment_.RunUntilIdle(); |
| |
| // Grab the context ID. |
| int id = worklet_impl->context_group_id_for_testing(); |
| |
| // Delete the worklet. |
| worklet.reset(); |
| task_environment_.RunUntilIdle(); |
| |
| // Try to resume post-delete. Should not crash |
| v8_helper_->v8_runner()->PostTask( |
| FROM_HERE, base::BindOnce([](scoped_refptr<AuctionV8Helper> v8_helper, |
| int id) { v8_helper->Resume(id); }, |
| v8_helper_, id)); |
| |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SellerWorkletTest, BasicV8Debug) { |
| ScopedInspectorSupport inspector_support(v8_helper_.get()); |
| |
| // Helper for looking for scriptParsed events. |
| auto is_script_parsed = [](const TestChannel::Event& event) -> bool { |
| if (event.type != TestChannel::Event::Type::Notification) |
| return false; |
| |
| const std::string* candidate_method = |
| event.value.GetDict().FindString("method"); |
| return (candidate_method && *candidate_method == "Debugger.scriptParsed"); |
| }; |
| |
| const GURL kUrl1 = GURL("http://example.com/first.js"); |
| const GURL kUrl2 = GURL("http://example.org/second.js"); |
| |
| AddJavascriptResponse(&url_loader_factory_, kUrl1, CreateScoreAdScript("1")); |
| AddJavascriptResponse(&url_loader_factory_, kUrl2, CreateScoreAdScript("2")); |
| |
| SellerWorklet* worklet_impl1 = nullptr; |
| decision_logic_url_ = kUrl1; |
| auto worklet1 = CreateWorklet( |
| /*pause_for_debugger_on_start=*/true, &worklet_impl1); |
| base::RunLoop run_loop1; |
| RunScoreAdOnWorkletAsync(worklet1.get(), /*expected_score=*/1, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop1.QuitClosure()); |
| |
| decision_logic_url_ = kUrl2; |
| SellerWorklet* worklet_impl2 = nullptr; |
| auto worklet2 = CreateWorklet( |
| /*pause_for_debugger_on_start=*/true, &worklet_impl2); |
| base::RunLoop run_loop2; |
| RunScoreAdOnWorkletAsync(worklet2.get(), /*expected_score=*/2, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop2.QuitClosure()); |
| |
| int id1 = worklet_impl1->context_group_id_for_testing(); |
| int id2 = worklet_impl2->context_group_id_for_testing(); |
| |
| TestChannel* channel1 = inspector_support.ConnectDebuggerSession(id1); |
| TestChannel* channel2 = inspector_support.ConnectDebuggerSession(id2); |
| |
| channel1->RunCommandAndWaitForResult( |
| 1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})"); |
| channel1->RunCommandAndWaitForResult( |
| 2, "Debugger.enable", |
| R"({"id":2,"method":"Debugger.enable","params":{}})"); |
| |
| channel2->RunCommandAndWaitForResult( |
| 1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})"); |
| channel2->RunCommandAndWaitForResult( |
| 2, "Debugger.enable", |
| R"({"id":2,"method":"Debugger.enable","params":{}})"); |
| |
| // Should not see scriptParsed before resume. |
| std::list<TestChannel::Event> events1 = channel1->TakeAllEvents(); |
| EXPECT_TRUE(base::ranges::none_of(events1, is_script_parsed)); |
| |
| // Unpause execution for #1. |
| EXPECT_FALSE(run_loop1.AnyQuitCalled()); |
| channel1->RunCommandAndWaitForResult( |
| 3, "Runtime.runIfWaitingForDebugger", |
| R"({"id":3,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| run_loop1.Run(); |
| |
| // channel1 should have had a parsed notification for kUrl1. |
| TestChannel::Event script_parsed1 = |
| channel1->WaitForMethodNotification("Debugger.scriptParsed"); |
| const std::string* url1 = |
| script_parsed1.value.GetDict().FindStringByDottedPath("params.url"); |
| ASSERT_TRUE(url1); |
| EXPECT_EQ(kUrl1.spec(), *url1); |
| |
| // There shouldn't be a parsed notification on channel 2, however. |
| std::list<TestChannel::Event> events2 = channel2->TakeAllEvents(); |
| EXPECT_TRUE(base::ranges::none_of(events2, is_script_parsed)); |
| |
| // Unpause execution for #2. |
| EXPECT_FALSE(run_loop2.AnyQuitCalled()); |
| channel2->RunCommandAndWaitForResult( |
| 3, "Runtime.runIfWaitingForDebugger", |
| R"({"id":3,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| run_loop2.Run(); |
| |
| // channel2 should have had a parsed notification for kUrl2. |
| TestChannel::Event script_parsed2 = |
| channel2->WaitForMethodNotification("Debugger.scriptParsed"); |
| const std::string* url2 = |
| script_parsed2.value.GetDict().FindStringByDottedPath("params.url"); |
| ASSERT_TRUE(url2); |
| EXPECT_EQ(kUrl2, *url2); |
| |
| worklet1.reset(); |
| worklet2.reset(); |
| task_environment_.RunUntilIdle(); |
| |
| // No other scriptParsed events should be on either channel. |
| events1 = channel1->TakeAllEvents(); |
| events2 = channel2->TakeAllEvents(); |
| EXPECT_TRUE(base::ranges::none_of(events1, is_script_parsed)); |
| EXPECT_TRUE(base::ranges::none_of(events2, is_script_parsed)); |
| } |
| |
| TEST_F(SellerWorkletTest, ParseErrorV8Debug) { |
| ScopedInspectorSupport inspector_support(v8_helper_.get()); |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| "Invalid Javascript"); |
| SellerWorklet* worklet_impl = nullptr; |
| auto worklet = |
| CreateWorklet(/*pause_for_debugger_on_start=*/true, &worklet_impl); |
| int id = worklet_impl->context_group_id_for_testing(); |
| TestChannel* channel = inspector_support.ConnectDebuggerSession(id); |
| |
| channel->RunCommandAndWaitForResult( |
| 1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})"); |
| channel->RunCommandAndWaitForResult( |
| 2, "Debugger.enable", |
| R"({"id":2,"method":"Debugger.enable","params":{}})"); |
| |
| // Unpause execution and wait for the pipe to be closed with an error. |
| channel->RunCommandAndWaitForResult( |
| 3, "Runtime.runIfWaitingForDebugger", |
| R"({"id":3,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| EXPECT_FALSE(WaitForDisconnect().empty()); |
| |
| // Should have gotten a parse error notification. |
| TestChannel::Event parse_error = |
| channel->WaitForMethodNotification("Debugger.scriptFailedToParse"); |
| const std::string* error_url = |
| parse_error.value.GetDict().FindStringByDottedPath("params.url"); |
| ASSERT_TRUE(error_url); |
| EXPECT_EQ(decision_logic_url_.spec(), *error_url); |
| } |
| |
| TEST_F(SellerWorkletTest, BasicDevToolsDebug) { |
| const char kScriptResult[] = "this.global_score ? this.global_score : 10"; |
| |
| const char kUrl1[] = "http://example.com/first.js"; |
| const char kUrl2[] = "http://example.org/second.js"; |
| |
| AddJavascriptResponse(&url_loader_factory_, GURL(kUrl1), |
| CreateScoreAdScript(kScriptResult)); |
| AddJavascriptResponse(&url_loader_factory_, GURL(kUrl2), |
| CreateScoreAdScript(kScriptResult)); |
| |
| decision_logic_url_ = GURL(kUrl1); |
| auto worklet1 = CreateWorklet(/*pause_for_debugger_on_start=*/true); |
| base::RunLoop run_loop1; |
| RunScoreAdOnWorkletAsync(worklet1.get(), /*expected_score=*/100.5, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop1.QuitClosure()); |
| |
| decision_logic_url_ = GURL(kUrl2); |
| auto worklet2 = CreateWorklet(/*pause_for_debugger_on_start=*/true); |
| base::RunLoop run_loop2; |
| RunScoreAdOnWorkletAsync(worklet2.get(), /*expected_score=*/0, |
| {"http://example.org/second.js scoreAd() did not " |
| "return an object or a number."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop2.QuitClosure()); |
| |
| mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent1, agent2; |
| worklet1->ConnectDevToolsAgent(agent1.BindNewEndpointAndPassReceiver()); |
| worklet2->ConnectDevToolsAgent(agent2.BindNewEndpointAndPassReceiver()); |
| |
| TestDevToolsAgentClient debug1(std::move(agent1), "123", |
| /*use_binary_protocol=*/true); |
| TestDevToolsAgentClient debug2(std::move(agent2), "456", |
| /*use_binary_protocol=*/true); |
| |
| debug1.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable", |
| R"({"id":1,"method":"Runtime.enable","params":{}})"); |
| debug1.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable", |
| R"({"id":2,"method":"Debugger.enable","params":{}})"); |
| |
| debug2.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable", |
| R"({"id":1,"method":"Runtime.enable","params":{}})"); |
| debug2.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable", |
| R"({"id":2,"method":"Debugger.enable","params":{}})"); |
| |
| const char kBreakpointTemplate[] = R"({ |
| "id":3, |
| "method":"Debugger.setBreakpointByUrl", |
| "params": { |
| "lineNumber": 2, |
| "url": "%s", |
| "columnNumber": 0, |
| "condition": "" |
| }})"; |
| |
| debug1.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 3, "Debugger.setBreakpointByUrl", |
| base::StringPrintf(kBreakpointTemplate, kUrl1)); |
| debug2.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 3, "Debugger.setBreakpointByUrl", |
| base::StringPrintf(kBreakpointTemplate, kUrl2)); |
| |
| // Now start #1. We should see a scriptParsed event. |
| debug1.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 4, |
| "Runtime.runIfWaitingForDebugger", |
| R"({"id":4,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| |
| TestDevToolsAgentClient::Event script_parsed1 = |
| debug1.WaitForMethodNotification("Debugger.scriptParsed"); |
| const std::string* url1 = |
| script_parsed1.value.GetDict().FindStringByDottedPath("params.url"); |
| ASSERT_TRUE(url1); |
| EXPECT_EQ(*url1, kUrl1); |
| |
| // Next there is the breakpoint. |
| TestDevToolsAgentClient::Event breakpoint_hit1 = |
| debug1.WaitForMethodNotification("Debugger.paused"); |
| |
| base::Value::List* hit_breakpoints = |
| breakpoint_hit1.value.GetDict().FindDict("params")->FindList( |
| "hitBreakpoints"); |
| ASSERT_TRUE(hit_breakpoints); |
| ASSERT_EQ(1u, hit_breakpoints->size()); |
| ASSERT_TRUE((*hit_breakpoints)[0].is_string()); |
| EXPECT_EQ("1:2:0:http://example.com/first.js", |
| (*hit_breakpoints)[0].GetString()); |
| std::string* callframe_id1 = breakpoint_hit1.value.GetDict() |
| .FindDict("params") |
| ->FindList("callFrames") |
| ->front() |
| .GetDict() |
| .FindString("callFrameId"); |
| |
| // Override the score value. |
| const char kCommandTemplate[] = R"({ |
| "id": 5, |
| "method": "Debugger.evaluateOnCallFrame", |
| "params": { |
| "callFrameId": "%s", |
| "expression": "global_score = %s" |
| } |
| })"; |
| |
| debug1.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kIO, 5, "Debugger.evaluateOnCallFrame", |
| base::StringPrintf(kCommandTemplate, callframe_id1->c_str(), "100.5")); |
| |
| // Let worklet 1 finish. The callback set by RunScoreAdOnWorkletAsync() will |
| // verify the result. |
| EXPECT_FALSE(run_loop1.AnyQuitCalled()); |
| debug1.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kIO, 6, "Debugger.resume", |
| R"({"id":6,"method":"Debugger.resume","params":{}})"); |
| run_loop1.Run(); |
| |
| // Start #2, see that it parses the script. |
| debug2.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 4, |
| "Runtime.runIfWaitingForDebugger", |
| R"({"id":4,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| |
| TestDevToolsAgentClient::Event script_parsed2 = |
| debug2.WaitForMethodNotification("Debugger.scriptParsed"); |
| const std::string* url2 = |
| script_parsed2.value.GetDict().FindStringByDottedPath("params.url"); |
| ASSERT_TRUE(url2); |
| EXPECT_EQ(*url2, kUrl2); |
| |
| // Wait for breakpoint, and then change the result to be trouble. |
| TestDevToolsAgentClient::Event breakpoint_hit2 = |
| debug2.WaitForMethodNotification("Debugger.paused"); |
| std::string* callframe_id2 = breakpoint_hit2.value.GetDict() |
| .FindDict("params") |
| ->FindList("callFrames") |
| ->front() |
| .GetDict() |
| .FindString("callFrameId"); |
| debug2.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kIO, 5, "Debugger.evaluateOnCallFrame", |
| base::StringPrintf(kCommandTemplate, callframe_id2->c_str(), |
| R"(\"not a score\")")); |
| |
| // Let worklet 2 finish. The callback set by RunScoreAdOnWorkletAsync() will |
| // verify the result. |
| debug2.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kIO, 6, "Debugger.resume", |
| R"({"id":6,"method":"Debugger.resume","params":{}})"); |
| run_loop2.Run(); |
| } |
| |
| TEST_F(SellerWorkletTest, InstrumentationBreakpoints) { |
| const char kUrl[] = "http://example.com/script.js"; |
| |
| std::string script_body = |
| CreateBasicSellAdScript() + |
| CreateReportToScript("1", R"(sendReportTo("https://foo.test"))"); |
| AddJavascriptResponse(&url_loader_factory_, GURL(kUrl), script_body); |
| |
| decision_logic_url_ = GURL(kUrl); |
| auto worklet = CreateWorklet(/*pause_for_debugger_on_start=*/true); |
| base::RunLoop run_loop; |
| RunScoreAdOnWorkletAsync(worklet.get(), /*expected_score=*/1.0, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop.QuitClosure()); |
| |
| mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent; |
| worklet->ConnectDevToolsAgent(agent.BindNewEndpointAndPassReceiver()); |
| |
| TestDevToolsAgentClient debug(std::move(agent), "123", |
| /*use_binary_protocol=*/true); |
| |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable", |
| R"({"id":1,"method":"Runtime.enable","params":{}})"); |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable", |
| R"({"id":2,"method":"Debugger.enable","params":{}})"); |
| |
| // Set the instrumentation breakpoints. |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 3, |
| "EventBreakpoints.setInstrumentationBreakpoint", |
| MakeInstrumentationBreakpointCommand(3, "set", |
| "beforeSellerWorkletScoringStart")); |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 4, |
| "EventBreakpoints.setInstrumentationBreakpoint", |
| MakeInstrumentationBreakpointCommand( |
| 4, "set", "beforeSellerWorkletReportingStart")); |
| |
| // Resume creation, ScoreAd() call should hit a breakpoint. |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 5, |
| "Runtime.runIfWaitingForDebugger", |
| R"({"id":5,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| |
| TestDevToolsAgentClient::Event breakpoint_hit1 = |
| debug.WaitForMethodNotification("Debugger.paused"); |
| |
| const std::string* breakpoint1 = |
| breakpoint_hit1.value.GetDict().FindStringByDottedPath( |
| "params.data.eventName"); |
| ASSERT_TRUE(breakpoint1); |
| EXPECT_EQ("instrumentation:beforeSellerWorkletScoringStart", *breakpoint1); |
| |
| // Let scoring finish. |
| EXPECT_FALSE(run_loop.AnyQuitCalled()); |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kIO, 6, "Debugger.resume", |
| R"({"id":6,"method":"Debugger.resume","params":{}})"); |
| run_loop.Run(); |
| |
| // Now try reporting, should hit the other breakpoint. |
| base::RunLoop run_loop2; |
| RunReportResultExpectingResultAsync( |
| worklet.get(), "1", GURL("https://foo.test/"), |
| /*expected_ad_beacon_map=*/{}, /*expected_pa_requests=*/{}, |
| /*expected_errors=*/{}, run_loop2.QuitClosure()); |
| TestDevToolsAgentClient::Event breakpoint_hit2 = |
| debug.WaitForMethodNotification("Debugger.paused"); |
| const std::string* breakpoint2 = |
| breakpoint_hit2.value.GetDict().FindStringByDottedPath( |
| "params.data.eventName"); |
| ASSERT_TRUE(breakpoint2); |
| EXPECT_EQ("instrumentation:beforeSellerWorkletReportingStart", *breakpoint2); |
| |
| // Let reporting finish. |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kIO, 7, "Debugger.resume", |
| R"({"id":7,"method":"Debugger.resume","params":{}})"); |
| run_loop2.Run(); |
| |
| // Running another scoreAd will trigger the breakpoint again, since we didn't |
| // remove it. |
| base::RunLoop run_loop3; |
| RunScoreAdOnWorkletAsync(worklet.get(), /*expected_score=*/1.0, |
| /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, |
| run_loop3.QuitClosure()); |
| |
| TestDevToolsAgentClient::Event breakpoint_hit3 = |
| debug.WaitForMethodNotification("Debugger.paused"); |
| |
| const std::string* breakpoint3 = |
| breakpoint_hit1.value.GetDict().FindStringByDottedPath( |
| "params.data.eventName"); |
| ASSERT_TRUE(breakpoint3); |
| EXPECT_EQ("instrumentation:beforeSellerWorkletScoringStart", *breakpoint3); |
| |
| // Let this round of scoring finish, too. |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kIO, 8, "Debugger.resume", |
| R"({"id":8,"method":"Debugger.resume","params":{}})"); |
| run_loop3.Run(); |
| } |
| |
| TEST_F(SellerWorkletTest, UnloadWhilePaused) { |
| // Make sure things are cleaned up properly if the worklet is destroyed while |
| // paused on a breakpoint. |
| const char kUrl[] = "http://example.com/script.js"; |
| |
| std::string script_body = |
| CreateBasicSellAdScript() + |
| CreateReportToScript("1", R"(sendReportTo("https://foo.test"))"); |
| AddJavascriptResponse(&url_loader_factory_, GURL(kUrl), script_body); |
| |
| decision_logic_url_ = GURL(kUrl); |
| auto worklet = CreateWorklet(/*pause_for_debugger_on_start=*/true); |
| RunScoreAdOnWorkletExpectingCallbackNeverInvoked(worklet.get()); |
| |
| mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent; |
| worklet->ConnectDevToolsAgent(agent.BindNewEndpointAndPassReceiver()); |
| |
| TestDevToolsAgentClient debug(std::move(agent), "123", |
| /*use_binary_protocol=*/true); |
| |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable", |
| R"({"id":1,"method":"Runtime.enable","params":{}})"); |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable", |
| R"({"id":2,"method":"Debugger.enable","params":{}})"); |
| |
| // Set the instrumentation breakpoint. |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 3, |
| "EventBreakpoints.setInstrumentationBreakpoint", |
| MakeInstrumentationBreakpointCommand(3, "set", |
| "beforeSellerWorkletScoringStart")); |
| // Resume execution of create. Should hit corresponding breakpoint. |
| debug.RunCommandAndWaitForResult( |
| TestDevToolsAgentClient::Channel::kMain, 4, |
| "Runtime.runIfWaitingForDebugger", |
| R"({"id":4,"method":"Runtime.runIfWaitingForDebugger","params":{}})"); |
| |
| RunScoreAdOnWorkletAsync( |
| worklet.get(), /*expected_score=*/1.0, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/ |
| mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}, |
| /*expected_bid_in_seller_currency=*/absl::nullopt, base::BindOnce([]() { |
| ADD_FAILURE() << "scoreAd shouldn't actually get to finish."; |
| })); |
| |
| debug.WaitForMethodNotification("Debugger.paused"); |
| |
| // Destroy the worklet |
| worklet.reset(); |
| |
| // This won't terminate if the V8 thread is still blocked in debugger. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| // Test that cancelling the worklet before it runs but after the execution was |
| // queued actually cancels the execution. This is done by trying to run a |
| // while(true) {} script with a timeout that's bigger than the test timeout, so |
| // if it doesn't get cancelled the *test* will timeout. |
| TEST_F(SellerWorkletTest, Cancelation) { |
| seller_timeout_ = base::Days(360); |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| "while(true) {}"); |
| mojo::Remote<mojom::SellerWorklet> seller_worklet = CreateWorklet(); |
| // Let the script load. |
| task_environment_.RunUntilIdle(); |
| |
| // Now we no longer need it for parsing JS, wedge the V8 thread so we get a |
| // chance to cancel the script *before* it actually tries running. |
| base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get()); |
| |
| TestScoreAdClient client(TestScoreAdClient::ScoreAdNeverInvokedCallback()); |
| mojo::Receiver<mojom::ScoreAdClient> client_receiver(&client); |
| |
| seller_worklet->ScoreAd( |
| ad_metadata_, bid_, bid_currency_, auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), component_expect_bid_currency_, |
| browser_signal_interest_group_owner_, browser_signal_render_url_, |
| browser_signal_ad_components_, browser_signal_bidding_duration_msecs_, |
| seller_timeout_, |
| /*trace_id=*/1, client_receiver.BindNewPipeAndPassRemote()); |
| |
| // Cancel and then unwedge. |
| client_receiver.reset(); |
| base::RunLoop().RunUntilIdle(); |
| event_handle->Signal(); |
| |
| // Make sure cancellation happens before ~SellerWorklet. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| // Test that queued tasks get cancelled at worklet destruction. |
| TEST_F(SellerWorkletTest, CancelationDtor) { |
| seller_timeout_ = base::Days(360); |
| |
| // ReportResult timeout isn't configurable the way scoreAd is. |
| v8_helper_->v8_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](scoped_refptr<AuctionV8Helper> v8_helper) { |
| v8_helper->set_script_timeout_for_testing(base::Days(360)); |
| }, |
| v8_helper_)); |
| |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| "while(true) {}"); |
| mojo::Remote<mojom::SellerWorklet> seller_worklet = CreateWorklet(); |
| // Let the script load. |
| task_environment_.RunUntilIdle(); |
| |
| // Now we no longer need it for parsing JS, wedge the V8 thread so we get a |
| // chance to cancel the script *before* it actually tries running. |
| base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get()); |
| |
| RunScoreAdOnWorkletExpectingCallbackNeverInvoked(seller_worklet.get()); |
| RunReportResultExpectingCallbackNeverInvoked(seller_worklet.get()); |
| |
| // Destroy the worklet, then unwedge. |
| seller_worklet.reset(); |
| base::RunLoop().RunUntilIdle(); |
| event_handle->Signal(); |
| } |
| |
| // Test that cancelling execution before the script is fetched doesn't run it. |
| TEST_F(SellerWorkletTest, CancelBeforeFetch) { |
| seller_timeout_ = base::Days(360); |
| |
| mojo::Remote<mojom::SellerWorklet> seller_worklet = CreateWorklet(); |
| TestScoreAdClient client(TestScoreAdClient::ScoreAdNeverInvokedCallback()); |
| mojo::Receiver<mojom::ScoreAdClient> client_receiver(&client); |
| |
| seller_worklet->ScoreAd( |
| ad_metadata_, bid_, bid_currency_, auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), component_expect_bid_currency_, |
| browser_signal_interest_group_owner_, browser_signal_render_url_, |
| browser_signal_ad_components_, browser_signal_bidding_duration_msecs_, |
| seller_timeout_, |
| /*trace_id=*/1, client_receiver.BindNewPipeAndPassRemote()); |
| task_environment_.RunUntilIdle(); |
| // Cancel and then make the script available. |
| client_receiver.reset(); |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| "while (true) {}"); |
| |
| // Make sure cancellation happens before ~SellerWorklet. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(SellerWorkletTest, ForDebuggingOnlyReportsDisabled) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"))"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", R"(forDebuggingOnly.reportAdAuctionWin("https://win.url"))"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt); |
| } |
| |
| class SellerWorkletSharedStorageAPIDisabledTest : public SellerWorkletTest { |
| public: |
| SellerWorkletSharedStorageAPIDisabledTest() { |
| feature_list_.InitAndDisableFeature(blink::features::kSharedStorageAPI); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| TEST_F(SellerWorkletSharedStorageAPIDisabledTest, SharedStorageNotExposed) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", /*extra_code=*/R"( |
| sharedStorage.clear(); |
| )"), |
| /*expected_score=*/0, /*expected_errors=*/ |
| {"https://url.test/:5 Uncaught ReferenceError: sharedStorage is not " |
| "defined."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"( |
| sharedStorage.clear(); |
| )", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| /*expected_errors=*/ |
| {"https://url.test/:11 Uncaught ReferenceError: sharedStorage is not " |
| "defined."}); |
| } |
| |
| class SellerWorkletSharedStorageAPIEnabledTest : public SellerWorkletTest { |
| public: |
| SellerWorkletSharedStorageAPIEnabledTest() { |
| feature_list_.InitAndEnableFeature(blink::features::kSharedStorageAPI); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| TEST_F(SellerWorkletSharedStorageAPIEnabledTest, SharedStorageWriteInScoreAd) { |
| auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host; |
| |
| { |
| mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver( |
| &test_shared_storage_host); |
| shared_storage_host_remote_ = receiver.BindNewPipeAndPassRemote(); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", /*extra_code=*/R"( |
| sharedStorage.set('a', 'b'); |
| sharedStorage.set('a', 'b', {ignoreIfPresent: true}); |
| sharedStorage.append('a', 'b'); |
| sharedStorage.delete('a'); |
| sharedStorage.clear(); |
| )"), |
| 5, /*expected_errors=*/ |
| {}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}); |
| |
| // Make sure the shared storage mojom methods are invoked as they use a |
| // dedicated pipe. |
| task_environment_.RunUntilIdle(); |
| |
| using RequestType = |
| auction_worklet::TestAuctionSharedStorageHost::RequestType; |
| using Request = auction_worklet::TestAuctionSharedStorageHost::Request; |
| |
| EXPECT_THAT(test_shared_storage_host.observed_requests(), |
| testing::ElementsAre(Request{.type = RequestType::kSet, |
| .key = u"a", |
| .value = u"b", |
| .ignore_if_present = false}, |
| Request{.type = RequestType::kSet, |
| .key = u"a", |
| .value = u"b", |
| .ignore_if_present = true}, |
| Request{.type = RequestType::kAppend, |
| .key = u"a", |
| .value = u"b", |
| .ignore_if_present = false}, |
| Request{.type = RequestType::kDelete, |
| .key = u"a", |
| .value = std::u16string(), |
| .ignore_if_present = false}, |
| Request{.type = RequestType::kClear, |
| .key = std::u16string(), |
| .value = std::u16string(), |
| .ignore_if_present = false})); |
| } |
| |
| { |
| shared_storage_host_remote_ = |
| mojo::PendingRemote<mojom::AuctionSharedStorageHost>(); |
| |
| // Set the shared-storage permissions policy to disallowed. |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/true, |
| /*shared_storage_allowed=*/false); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", /*extra_code=*/R"( |
| sharedStorage.clear(); |
| )"), |
| /*expected_score=*/0, /*expected_errors=*/ |
| {"https://url.test/:5 Uncaught TypeError: The \"shared-storage\" " |
| "Permissions Policy denied the method on sharedStorage."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}); |
| |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/true, |
| /*shared_storage_allowed=*/true); |
| } |
| } |
| |
| TEST_F(SellerWorkletSharedStorageAPIEnabledTest, |
| SharedStorageWriteInReportResult) { |
| auction_worklet::TestAuctionSharedStorageHost test_shared_storage_host; |
| |
| { |
| mojo::Receiver<auction_worklet::mojom::AuctionSharedStorageHost> receiver( |
| &test_shared_storage_host); |
| shared_storage_host_remote_ = receiver.BindNewPipeAndPassRemote(); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"( |
| sharedStorage.set('a', 'b'); |
| sharedStorage.set('a', 'b', {ignoreIfPresent: true}); |
| sharedStorage.append('a', 'b'); |
| sharedStorage.delete('a'); |
| sharedStorage.clear(); |
| )", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| /*expected_errors=*/{}); |
| |
| // Make sure the shared storage mojom methods are invoked as they use a |
| // dedicated pipe. |
| task_environment_.RunUntilIdle(); |
| |
| using RequestType = |
| auction_worklet::TestAuctionSharedStorageHost::RequestType; |
| using Request = auction_worklet::TestAuctionSharedStorageHost::Request; |
| |
| EXPECT_THAT(test_shared_storage_host.observed_requests(), |
| testing::ElementsAre(Request{.type = RequestType::kSet, |
| .key = u"a", |
| .value = u"b", |
| .ignore_if_present = false}, |
| Request{.type = RequestType::kSet, |
| .key = u"a", |
| .value = u"b", |
| .ignore_if_present = true}, |
| Request{.type = RequestType::kAppend, |
| .key = u"a", |
| .value = u"b", |
| .ignore_if_present = false}, |
| Request{.type = RequestType::kDelete, |
| .key = u"a", |
| .value = std::u16string(), |
| .ignore_if_present = false}, |
| Request{.type = RequestType::kClear, |
| .key = std::u16string(), |
| .value = std::u16string(), |
| .ignore_if_present = false})); |
| } |
| |
| { |
| shared_storage_host_remote_ = |
| mojo::PendingRemote<mojom::AuctionSharedStorageHost>(); |
| |
| // Set the shared-storage permissions policy to disallowed. |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/true, |
| /*shared_storage_allowed=*/false); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"( |
| sharedStorage.clear(); |
| )", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| /*expected_errors=*/ |
| {"https://url.test/:11 Uncaught TypeError: The \"shared-storage\" " |
| "Permissions Policy denied the method on sharedStorage."}); |
| |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/true, |
| /*shared_storage_allowed=*/true); |
| } |
| } |
| |
| class SellerWorkletRealTimeTest : public SellerWorkletTest { |
| public: |
| SellerWorkletRealTimeTest() |
| : SellerWorkletTest( |
| base::test::TaskEnvironment::TimeSource::SYSTEM_TIME) {} |
| }; |
| |
| // `scoreAd` should time out due to AuctionV8Helper's default script timeout (50 |
| // ms). |
| TEST_F(SellerWorkletRealTimeTest, ScoreAdTimedOut) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript(/*raw_return_value=*/"", R"(while (1))"), 0, |
| {"https://url.test/ execution of `scoreAd` timed out."}); |
| } |
| |
| TEST_F(SellerWorkletRealTimeTest, ScoreAdSellerTimeoutFromAuctionConfig) { |
| // Use a very long default script timeout, and a short seller timeout, so |
| // that if the seller script with endless loop times out, we know that the |
| // seller 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(); |
| |
| seller_timeout_ = base::Milliseconds(20); |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript(/*raw_return_value=*/"", R"(while (1))"), 0, |
| {"https://url.test/ execution of `scoreAd` timed out."}); |
| } |
| |
| class SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest |
| : public SellerWorkletRealTimeTest { |
| public: |
| SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest() { |
| feature_list_.InitAndEnableFeature( |
| blink::features::kBiddingAndScoringDebugReportingAPI); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Test forDebuggingOnly.reportAdAuctionLoss() and |
| // forDebuggingOnly.reportAdAuctionWin() called in scoreAd(). |
| TEST_F(SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReports) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"); |
| forDebuggingOnly.reportAdAuctionWin("https://win.url"))"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, GURL("https://loss.url"), |
| GURL("https://win.url")); |
| |
| // Should keep debug report URLs when score <= 0. |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "-1", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"); |
| forDebuggingOnly.reportAdAuctionWin("https://win.url"))"), |
| 0, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, GURL("https://loss.url"), |
| GURL("https://win.url")); |
| |
| // It's OK to call one API but not the other. |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"))"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, GURL("https://loss.url")); |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", R"(forDebuggingOnly.reportAdAuctionWin("https://win.url"))"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| GURL("https://win.url")); |
| |
| // There should be no debugging report URLs when scoreAd() returns invalid |
| // value type. |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "\"invalid_score\"", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"); |
| forDebuggingOnly.reportAdAuctionWin("https://win.url"))"), |
| 0, {"https://url.test/ scoreAd() did not return an object or a number."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt); |
| } |
| |
| // Debugging loss/win report URLs should be nullopt if scoreAd() pareamters are |
| // invalid. |
| TEST_F(SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportsInvalidScoreAdParameter) { |
| // Auction config param is invalid. |
| auction_ad_config_non_shared_params_.auction_signals = |
| blink::AuctionConfig::MaybePromiseJson::FromValue("{invalid json"); |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"); |
| forDebuggingOnly.reportAdAuctionWin("https://win.url"))"), |
| 0); |
| // Setting it back to default value to avoid affecting following tests. |
| auction_ad_config_non_shared_params_.auction_signals = |
| blink::AuctionConfig::MaybePromiseJson::FromValue( |
| R"({"is_auction_signals": true})"); |
| |
| // `ad_metadata_` is an invalid json. |
| ad_metadata_ = "{invalid_json"; |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"); |
| forDebuggingOnly.reportAdAuctionWin("https://win.url"))"), |
| 0); |
| } |
| |
| TEST_F(SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportsInvalidParameter) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("1", R"(forDebuggingOnly.reportAdAuctionLoss(null))"), |
| 0, |
| {"https://url.test/:4 Uncaught TypeError: " |
| "reportAdAuctionLoss requires 1 string parameter."}); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("1", R"(forDebuggingOnly.reportAdAuctionWin([5]))"), |
| 0, |
| {"https://url.test/:4 Uncaught TypeError: " |
| "reportAdAuctionWin requires 1 string parameter."}); |
| |
| std::vector<std::string> non_https_urls = {"http://report.url", |
| "file:///foo/", "Not a URL"}; |
| for (const auto& url : non_https_urls) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| base::StringPrintf(R"(forDebuggingOnly.reportAdAuctionLoss("%s"))", |
| url.c_str())), |
| 0, |
| {"https://url.test/:4 Uncaught TypeError: " |
| "reportAdAuctionLoss must be passed a valid HTTPS url."}); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| base::StringPrintf(R"(forDebuggingOnly.reportAdAuctionWin("%s"))", |
| url.c_str())), |
| 0, |
| {"https://url.test/:4 Uncaught TypeError: " |
| "reportAdAuctionWin must be passed a valid HTTPS url."}); |
| } |
| |
| // No message if caught, but still no debug report URLs. |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| R"(try {forDebuggingOnly.reportAdAuctionLoss("http://loss.url")} |
| catch (e) {})"), |
| 1, /*expected_errors=*/{}); |
| } |
| |
| TEST_F(SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportsMultiCallsAllowed) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"); |
| forDebuggingOnly.reportAdAuctionLoss("https://loss.url2"))"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/GURL("https://loss.url2"), |
| /*expected_debug_win_report_url=*/absl::nullopt); |
| |
| // Test that the first URL is preserved when the second call throws. |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url"); |
| try { |
| forDebuggingOnly.reportAdAuctionLoss("http://invalidloss.url"); |
| } catch (e) {})"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/GURL("https://loss.url"), |
| /*expected_debug_win_report_url=*/absl::nullopt); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| R"(forDebuggingOnly.reportAdAuctionWin("https://win.url"); |
| forDebuggingOnly.reportAdAuctionWin("https://win.url2"))"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/GURL("https://win.url2")); |
| |
| // Test that the first URL is preserved when the second call throws. |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "1", |
| R"(forDebuggingOnly.reportAdAuctionWin("https://win.url"); |
| try { |
| forDebuggingOnly.reportAdAuctionWin("http://invalidwin.url"); |
| } catch (e) {})"), |
| 1, /*expected_errors=*/{}, mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/GURL("https://win.url")); |
| } |
| |
| // Loss report URLs before seller script times out should be kept. |
| TEST_F(SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest, |
| ScoreAdHasError) { |
| // The seller script has an endless while loop. It will time out due to |
| // AuctionV8Helper's default script timeout (50 ms). |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| /*raw_return_value=*/"", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url1"); |
| error; |
| forDebuggingOnly.reportAdAuctionLoss("https://loss.url2"))"), |
| 0, {"https://url.test/:5 Uncaught ReferenceError: error is not defined."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/{}, GURL("https://loss.url1")); |
| } |
| |
| // Loss report URLs before seller script times out should be kept. |
| TEST_F(SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest, |
| ScoreAdTimedOut) { |
| // The seller script has an endless while loop. It will time out due to |
| // AuctionV8Helper's default script timeout (50 ms). |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| /*raw_return_value=*/"", |
| R"(forDebuggingOnly.reportAdAuctionLoss("https://loss.url1"); |
| while (1); |
| forDebuggingOnly.reportAdAuctionLoss("https://loss.url2"))"), |
| 0, {"https://url.test/ execution of `scoreAd` timed out."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/{}, GURL("https://loss.url1")); |
| } |
| |
| // Subsequent runs of the same script should not affect each other. |
| TEST_F(SellerWorkletBiddingAndScoringDebugReportingAPIEnabledTest, |
| ForDebuggingOnlyReportsScriptIsolation) { |
| AddJavascriptResponse(&url_loader_factory_, decision_logic_url_, |
| R"( |
| function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, |
| browserSignals) { |
| if (bid === 1) { |
| forDebuggingOnly.reportAdAuctionLoss("https://loss.url"); |
| forDebuggingOnly.reportAdAuctionWin("https://win.url"); |
| } |
| return bid; |
| } |
| |
| function reportResult() {} |
| )"); |
| auto seller_worklet = CreateWorklet(); |
| ASSERT_TRUE(seller_worklet); |
| |
| // Run the same script twice, and only call debugging report in the first run. |
| // Only the first run will have debugging report URLs. |
| for (int i = 0; i < 2; ++i) { |
| base::RunLoop run_loop; |
| seller_worklet->ScoreAd( |
| ad_metadata_, i + 1, bid_currency_, |
| auction_ad_config_non_shared_params_, |
| direct_from_seller_seller_signals_, direct_from_seller_auction_signals_, |
| browser_signals_other_seller_.Clone(), component_expect_bid_currency_, |
| browser_signal_interest_group_owner_, browser_signal_render_url_, |
| browser_signal_ad_components_, browser_signal_bidding_duration_msecs_, |
| seller_timeout_, |
| /*trace_id=*/1, |
| TestScoreAdClient::Create(base::BindLambdaForTesting( |
| [&run_loop](double score, mojom::RejectReason reject_reason, |
| mojom::ComponentAuctionModifiedBidParamsPtr |
| component_auction_modified_bid_params, |
| absl::optional<double> bid_in_seller_currency, |
| absl::optional<uint32_t> scoring_signals_data_version, |
| const absl::optional<GURL>& debug_loss_report_url, |
| const absl::optional<GURL>& debug_win_report_url, |
| PrivateAggregationRequests pa_requests, |
| const std::vector<std::string>& errors) { |
| if (score == 1) { |
| EXPECT_TRUE(debug_loss_report_url.has_value()); |
| EXPECT_TRUE(debug_win_report_url.has_value()); |
| EXPECT_EQ(GURL("https://loss.url"), |
| debug_loss_report_url.value()); |
| EXPECT_EQ(GURL("https://win.url"), |
| debug_win_report_url.value()); |
| } else { |
| EXPECT_EQ(absl::nullopt, debug_loss_report_url); |
| EXPECT_EQ(absl::nullopt, debug_win_report_url); |
| } |
| run_loop.Quit(); |
| }))); |
| run_loop.Run(); |
| } |
| } |
| |
| class SellerWorkletPrivateAggregationEnabledTest : public SellerWorkletTest { |
| public: |
| SellerWorkletPrivateAggregationEnabledTest() { |
| scoped_feature_list_.InitAndEnableFeatureWithParameters( |
| blink::features::kPrivateAggregationApi, |
| {{"fledge_extensions_enabled", "true"}}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(SellerWorkletPrivateAggregationEnabledTest, ScoreAd) { |
| mojom::PrivateAggregationRequest kExpectedRequest1( |
| mojom::AggregatableReportContribution::NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| /*bucket=*/123, |
| /*value=*/45)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| mojom::PrivateAggregationRequest kExpectedRequest2( |
| mojom::AggregatableReportContribution::NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| /*bucket=*/absl::MakeInt128(/*high=*/1, /*low=*/0), |
| /*value=*/1)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| |
| mojom::PrivateAggregationRequest kExpectedForEventRequest1( |
| mojom::AggregatableReportContribution::NewForEventContribution( |
| mojom::AggregatableReportForEventContribution::New( |
| /*bucket=*/mojom::ForEventSignalBucket::NewIdBucket(234), |
| /*value=*/mojom::ForEventSignalValue::NewIntValue(56), |
| /*event_type=*/"reserved.win")), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| mojom::PrivateAggregationRequest kExpectedForEventRequest2( |
| mojom::AggregatableReportContribution::NewForEventContribution( |
| mojom::AggregatableReportForEventContribution::New( |
| /*bucket=*/mojom::ForEventSignalBucket::NewIdBucket( |
| absl::MakeInt128(/*high=*/1, |
| /*low=*/0)), |
| /*value=*/mojom::ForEventSignalValue::NewIntValue(2), |
| /*event_type=*/"reserved.win")), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest1.Clone()); |
| expected_pa_requests.push_back(kExpectedForEventRequest1.Clone()); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", R"( |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| privateAggregation.reportContributionForEvent( |
| "reserved.win", {bucket: 234n, value: 56}); |
| )"), |
| 5, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| std::move(expected_pa_requests)); |
| } |
| |
| // Set the private-aggregation permissions policy to disallowed. |
| { |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/false, |
| /*shared_storage_allowed=*/true); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", |
| "privateAggregation.sendHistogramReport({bucket: " |
| "123n, value: 45})"), |
| /*expected_score=*/0, /*expected_errors=*/ |
| {"https://url.test/:4 Uncaught TypeError: The \"private-aggregation\" " |
| "Permissions Policy denied the method on privateAggregation."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}); |
| |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/true, |
| /*shared_storage_allowed=*/true); |
| } |
| |
| // Large bucket |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest2.Clone()); |
| expected_pa_requests.push_back(kExpectedForEventRequest2.Clone()); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", R"( |
| privateAggregation.sendHistogramReport({bucket: 18446744073709551616n, |
| value: 1}); |
| privateAggregation.reportContributionForEvent( |
| "reserved.win", {bucket: 18446744073709551616n, value: 2}); |
| )"), |
| 5, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| std::move(expected_pa_requests)); |
| } |
| |
| // Multiple requests |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest1.Clone()); |
| expected_pa_requests.push_back(kExpectedRequest2.Clone()); |
| expected_pa_requests.push_back(kExpectedForEventRequest1.Clone()); |
| expected_pa_requests.push_back(kExpectedForEventRequest2.Clone()); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", R"( |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| privateAggregation.sendHistogramReport({bucket: 18446744073709551616n, |
| value: 1}); |
| privateAggregation.reportContributionForEvent( |
| "reserved.win", {bucket: 234n, value: 56}); |
| privateAggregation.reportContributionForEvent( |
| "reserved.win", {bucket: 18446744073709551616n, value: 2}); |
| )"), |
| 5, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| std::move(expected_pa_requests)); |
| } |
| |
| // An unrelated exception after sendHistogramReport and |
| // reportContributionForEvent shouldn't block the reports. |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest1.Clone()); |
| expected_pa_requests.push_back(kExpectedForEventRequest1.Clone()); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", R"( |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| privateAggregation.reportContributionForEvent( |
| "reserved.win", {bucket: 234n, value: 56}); |
| error; |
| )"), |
| 0, /*expected_errors=*/ |
| {"https://url.test/:8 Uncaught ReferenceError: error is not defined."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| std::move(expected_pa_requests)); |
| } |
| |
| // Debug mode enabled with debug key |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(mojom::PrivateAggregationRequest::New( |
| kExpectedRequest1.contribution->Clone(), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New( |
| /*is_enabled=*/true, blink::mojom::DebugKey::New(1234u)))); |
| expected_pa_requests.push_back(mojom::PrivateAggregationRequest::New( |
| kExpectedForEventRequest1.contribution->Clone(), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New( |
| /*is_enabled=*/true, blink::mojom::DebugKey::New(1234u)))); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", |
| R"( |
| privateAggregation.enableDebugMode({debug_key: 1234n}); |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| privateAggregation.reportContributionForEvent( |
| "reserved.win", {bucket: 234n, value: 56}); |
| )"), |
| 5, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| std::move(expected_pa_requests)); |
| } |
| |
| // Debug mode enabled without debug key, but with multiple requests |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(mojom::PrivateAggregationRequest::New( |
| kExpectedRequest1.contribution->Clone(), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New( |
| /*is_enabled=*/true, /*debug_key=*/nullptr))); |
| expected_pa_requests.push_back(mojom::PrivateAggregationRequest::New( |
| kExpectedRequest2.contribution->Clone(), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New( |
| /*is_enabled=*/true, /*debug_key=*/nullptr))); |
| |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript("5", |
| R"( |
| privateAggregation.enableDebugMode(); |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| privateAggregation.sendHistogramReport( |
| {bucket: 18446744073709551616n, value: 1}); |
| )"), |
| 5, /*expected_errors=*/{}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| std::move(expected_pa_requests)); |
| } |
| } |
| |
| TEST_F(SellerWorkletPrivateAggregationEnabledTest, ReportResult) { |
| mojom::PrivateAggregationRequest kExpectedRequest1( |
| mojom::AggregatableReportContribution::NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| /*bucket=*/123, |
| /*value=*/45)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| mojom::PrivateAggregationRequest kExpectedRequest2( |
| mojom::AggregatableReportContribution::NewHistogramContribution( |
| blink::mojom::AggregatableReportHistogramContribution::New( |
| /*bucket=*/absl::MakeInt128(/*high=*/1, /*low=*/0), |
| /*value=*/1)), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| mojom::PrivateAggregationRequest kExpectedForEventRequest( |
| mojom::AggregatableReportContribution::NewForEventContribution( |
| mojom::AggregatableReportForEventContribution::New( |
| /*bucket=*/mojom::ForEventSignalBucket::NewIdBucket(234), |
| /*value=*/mojom::ForEventSignalValue::NewIntValue(56), |
| /*event_type=*/"reserved.win")), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New()); |
| |
| // Only sendHistogramReport() is called. |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest1.Clone()); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(privateAggregation.sendHistogramReport({bucket: 123n, value: 45});)", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/{}); |
| } |
| |
| // Only reportContributionForEvent() is called. |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedForEventRequest.Clone()); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| "5", |
| R"( |
| privateAggregation.reportContributionForEvent( |
| "reserved.win", {bucket: 234n, value: 56}); |
| )", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/{}); |
| } |
| |
| // Both sendHistogramReport() and reportContributionForEvent() are called. |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest1.Clone()); |
| expected_pa_requests.push_back(kExpectedForEventRequest.Clone()); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| "5", |
| R"( |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| privateAggregation.reportContributionForEvent( |
| "reserved.win", {bucket: 234n, value: 56}); |
| )", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/{}); |
| } |
| |
| // Set the private-aggregation permissions policy to disallowed. |
| { |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/false, |
| /*shared_storage_allowed=*/true); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(privateAggregation.sendHistogramReport({bucket: 123n, value: 45});)", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| /*expected_errors=*/ |
| {"https://url.test/:10 Uncaught TypeError: The \"private-aggregation\" " |
| "Permissions Policy denied the method on privateAggregation."}); |
| |
| permissions_policy_state_ = |
| mojom::AuctionWorkletPermissionsPolicyState::New( |
| /*private_aggregation_allowed=*/true, |
| /*shared_storage_allowed=*/true); |
| } |
| |
| // BigInt bucket |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest1.Clone()); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(privateAggregation.sendHistogramReport({bucket: 123n, value: 45});)", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/{}); |
| } |
| |
| // Large bucket |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest2.Clone()); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(privateAggregation.sendHistogramReport( |
| {bucket: 18446744073709551616n, value: 1});)", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/{}); |
| } |
| |
| // Multiple requests |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest1.Clone()); |
| expected_pa_requests.push_back(kExpectedRequest2.Clone()); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"( |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| privateAggregation.sendHistogramReport({bucket: 18446744073709551616n, |
| value: 1}); |
| )", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/{}); |
| } |
| |
| // An unrelated exception after sendHistogramReport shouldn't block the report |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(kExpectedRequest1.Clone()); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"( |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| error; |
| )", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/ |
| {"https://url.test/:12 Uncaught ReferenceError: error is not " |
| "defined."}); |
| } |
| |
| // Debug mode enabled with debug key |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(mojom::PrivateAggregationRequest::New( |
| kExpectedRequest1.contribution->Clone(), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New( |
| /*is_enabled=*/true, blink::mojom::DebugKey::New(1234u)))); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| "5", |
| R"( |
| privateAggregation.enableDebugMode({debug_key: 1234n}); |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| )", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/{}); |
| } |
| |
| // Debug mode enabled without debug key, but with multiple requests |
| { |
| PrivateAggregationRequests expected_pa_requests; |
| expected_pa_requests.push_back(mojom::PrivateAggregationRequest::New( |
| kExpectedRequest1.contribution->Clone(), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New( |
| /*is_enabled=*/true, /*debug_key=*/nullptr))); |
| expected_pa_requests.push_back(mojom::PrivateAggregationRequest::New( |
| kExpectedRequest2.contribution->Clone(), |
| blink::mojom::AggregationServiceMode::kDefault, |
| blink::mojom::DebugModeDetails::New( |
| /*is_enabled=*/true, /*debug_key=*/nullptr))); |
| |
| RunReportResultCreatedScriptExpectingResult( |
| "5", |
| R"( |
| privateAggregation.enableDebugMode(); |
| privateAggregation.sendHistogramReport({bucket: 123n, value: 45}); |
| privateAggregation.sendHistogramReport( |
| {bucket: 18446744073709551616n, value: 1}); |
| )", |
| /*expected_signals_for_winner=*/"5", |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| std::move(expected_pa_requests), |
| /*expected_errors=*/{}); |
| } |
| |
| // Debug mode enabled twice |
| { |
| RunReportResultCreatedScriptExpectingResult( |
| "5", |
| R"( |
| privateAggregation.enableDebugMode(); |
| privateAggregation.enableDebugMode(); |
| )", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| /*expected_errors=*/ |
| {"https://url.test/:12 Uncaught TypeError: enableDebugMode may be " |
| "called at most once."}); |
| } |
| } |
| |
| class SellerWorkletPrivateAggregationDisabledTest : public SellerWorkletTest { |
| public: |
| SellerWorkletPrivateAggregationDisabledTest() { |
| scoped_feature_list_.InitAndDisableFeature( |
| blink::features::kPrivateAggregationApi); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| TEST_F(SellerWorkletPrivateAggregationDisabledTest, ScoreAd) { |
| RunScoreAdWithJavascriptExpectingResult( |
| CreateScoreAdScript( |
| "5", |
| "privateAggregation.sendHistogramReport({bucket: 123n, value: 45})"), |
| 0, /*expected_errors=*/ |
| {"https://url.test/:4 Uncaught ReferenceError: privateAggregation is not " |
| "defined."}, |
| mojom::ComponentAuctionModifiedBidParamsPtr(), |
| /*expected_data_version=*/absl::nullopt, |
| /*expected_debug_loss_report_url=*/absl::nullopt, |
| /*expected_debug_win_report_url=*/absl::nullopt, |
| /*expected_reject_reason=*/mojom::RejectReason::kNotAvailable, |
| /*expected_pa_requests=*/{}); |
| } |
| |
| TEST_F(SellerWorkletPrivateAggregationDisabledTest, ReportResult) { |
| RunReportResultCreatedScriptExpectingResult( |
| R"(5)", |
| R"(privateAggregation.sendHistogramReport({bucket: 123n, value: 45});)", |
| /*expected_signals_for_winner=*/absl::nullopt, |
| /*expected_report_url=*/absl::nullopt, /*expected_ad_beacon_map=*/{}, |
| /*expected_pa_requests=*/{}, |
| /*expected_errors=*/ |
| {"https://url.test/:10 Uncaught ReferenceError: privateAggregation is " |
| "not defined."}); |
| } |
| |
| } // namespace |
| } // namespace auction_worklet |